This commit is contained in:
sawyer-shi 2026-05-08 20:36:16 -05:00 committed by GitHub
commit 98ab55745d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 120 additions and 35 deletions

View File

@ -4,6 +4,7 @@ import { TaskStatus } from '@/app/components/plugins/types'
import { usePluginTaskStatus } from '../hooks'
const mockClearTask = vi.fn().mockResolvedValue({})
const mockStopAllTask = vi.fn().mockResolvedValue({})
const mockRefetch = vi.fn()
vi.mock('@/service/use-plugins', () => ({
@ -28,6 +29,9 @@ vi.mock('@/service/use-plugins', () => ({
useMutationClearTaskPlugin: () => ({
mutateAsync: mockClearTask,
}),
useMutationStopAllTaskPlugins: () => ({
mutateAsync: mockStopAllTask,
}),
}))
describe('usePluginTaskStatus', () => {
@ -74,4 +78,13 @@ describe('usePluginTaskStatus', () => {
})
expect(mockRefetch).toHaveBeenCalled()
})
it('should handle stop all plugins', async () => {
const { result } = renderHook(() => usePluginTaskStatus())
await result.current.handleStopAllPlugins()
expect(mockStopAllTask).toHaveBeenCalled()
expect(mockRefetch).toHaveBeenCalled()
})
})

View File

@ -3,7 +3,7 @@ import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { PluginSource, TaskStatus } from '@/app/components/plugins/types'
// Import mocked modules
import { useMutationClearTaskPlugin, usePluginTaskList } from '@/service/use-plugins'
import { useMutationClearTaskPlugin, useMutationStopAllTaskPlugins, usePluginTaskList } from '@/service/use-plugins'
import PluginTaskList from '../components/plugin-task-list'
import TaskStatusIndicator from '../components/task-status-indicator'
import { usePluginTaskStatus } from '../hooks'
@ -14,6 +14,7 @@ import PluginTasks from '../index'
vi.mock('@/service/use-plugins', () => ({
usePluginTaskList: vi.fn(),
useMutationClearTaskPlugin: vi.fn(),
useMutationStopAllTaskPlugins: vi.fn(),
}))
vi.mock('@/app/components/plugins/install-plugin/base/use-get-icon', () => ({
@ -45,6 +46,7 @@ const createMockPlugin = (overrides: Partial<PluginStatus> = {}): PluginStatus =
// Helper to setup mock hook returns
const setupMocks = (plugins: PluginStatus[] = []) => {
const mockMutateAsync = vi.fn().mockResolvedValue({})
const mockStopAllMutateAsync = vi.fn().mockResolvedValue({})
const mockHandleRefetch = vi.fn()
vi.mocked(usePluginTaskList).mockReturnValue({
@ -58,7 +60,11 @@ const setupMocks = (plugins: PluginStatus[] = []) => {
mutateAsync: mockMutateAsync,
} as unknown as ReturnType<typeof useMutationClearTaskPlugin>)
return { mockMutateAsync, mockHandleRefetch }
vi.mocked(useMutationStopAllTaskPlugins).mockReturnValue({
mutateAsync: mockStopAllMutateAsync,
} as unknown as ReturnType<typeof useMutationStopAllTaskPlugins>)
return { mockMutateAsync, mockStopAllMutateAsync, mockHandleRefetch }
}
const getTaskMenuTrigger = () =>
@ -273,6 +279,31 @@ describe('usePluginTaskStatus Hook', () => {
})
})
})
describe('handleStopAllPlugins', () => {
it('should call stop all mutateAsync and handleRefetch', async () => {
const { mockStopAllMutateAsync, mockHandleRefetch } = setupMocks([
createMockPlugin({ status: TaskStatus.running }),
])
const TestComponent = () => {
const { handleStopAllPlugins } = usePluginTaskStatus()
return (
<button onClick={handleStopAllPlugins}>
Stop all
</button>
)
}
render(<TestComponent />)
fireEvent.click(screen.getByRole('button'))
await waitFor(() => {
expect(mockStopAllMutateAsync).toHaveBeenCalledWith()
expect(mockHandleRefetch).toHaveBeenCalled()
})
})
})
})
// ============================================================================
@ -424,6 +455,7 @@ describe('PluginTaskList Component', () => {
onClearAll: vi.fn(),
onClearErrors: vi.fn(),
onClearSingle: vi.fn(),
onStopAll: vi.fn(),
}
beforeEach(() => {
@ -495,6 +527,23 @@ describe('PluginTaskList Component', () => {
expect(handleClearAll).toHaveBeenCalledTimes(1)
})
it('should call onStopAll when stop all button is clicked in running section', () => {
const handleStopAll = vi.fn()
const runningPlugins = [createMockPlugin({ status: TaskStatus.running })]
render(
<PluginTaskList
{...defaultProps}
runningPlugins={runningPlugins}
onStopAll={handleStopAll}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /task\.stopAll/i }))
expect(handleStopAll).toHaveBeenCalledTimes(1)
})
it('should call onClearErrors when clear all button is clicked in error section', () => {
const handleClearErrors = vi.fn()
const errorPlugins = [createMockPlugin({ status: TaskStatus.failed })]

View File

@ -57,6 +57,7 @@ describe('PluginTaskList', () => {
onClearAll: vi.fn(),
onClearErrors: vi.fn(),
onClearSingle: vi.fn(),
onStopAll: vi.fn(),
}
beforeEach(() => {
@ -180,44 +181,28 @@ describe('PluginTaskList', () => {
})
describe('Running section', () => {
it('should not render clear buttons for running plugins', () => {
it('should render stop all button for running plugins', () => {
render(<PluginTaskList {...defaultProps} runningPlugins={runningPlugins} />)
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
// Running section has no headerAction and no onClearSingle
expect(screen.getByRole('button', { name: /plugin\.task\.stopAll/ })).toBeInTheDocument()
expect(screen.queryByText(/plugin\.task\.clearAll/)).not.toBeInTheDocument()
})
it('should call onStopAll when stop all button is clicked', () => {
const onStopAll = vi.fn()
render(
<PluginTaskList
{...defaultProps}
runningPlugins={runningPlugins}
onStopAll={onStopAll}
/>,
)
fireEvent.click(screen.getByRole('button', { name: /plugin\.task\.stopAll/ }))
expect(onStopAll).toHaveBeenCalledTimes(1)
})
it('should show installing hint as status text', () => {
render(<PluginTaskList {...defaultProps} runningPlugins={runningPlugins} />)

View File

@ -15,6 +15,7 @@ type PluginTaskListProps = {
onClearAll: () => void
onClearErrors: () => void
onClearSingle: (taskId: string, pluginId: string) => void
onStopAll: () => void
}
const PluginTaskList: FC<PluginTaskListProps> = ({
@ -25,6 +26,7 @@ const PluginTaskList: FC<PluginTaskListProps> = ({
onClearAll,
onClearErrors,
onClearSingle,
onStopAll,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
@ -43,6 +45,16 @@ const PluginTaskList: FC<PluginTaskListProps> = ({
<span className="i-ri-loader-2-line h-3.5 w-3.5 animate-spin text-text-accent" />
}
defaultStatusText={t('task.installingHint', { ns: 'plugin' })}
headerAction={(
<Button
className="shrink-0"
size="small"
variant="ghost"
onClick={onStopAll}
>
{t('task.stopAll', { ns: 'plugin' })}
</Button>
)}
/>
)}

View File

@ -5,6 +5,7 @@ import {
import { TaskStatus } from '@/app/components/plugins/types'
import {
useMutationClearTaskPlugin,
useMutationStopAllTaskPlugins,
usePluginTaskList,
} from '@/service/use-plugins'
@ -14,6 +15,7 @@ export const usePluginTaskStatus = () => {
handleRefetch,
} = usePluginTaskList()
const { mutateAsync } = useMutationClearTaskPlugin()
const { mutateAsync: mutateStopAllAsync } = useMutationStopAllTaskPlugins()
const allPlugins = pluginTasks.map(task => task.plugins.map((plugin) => {
return {
...plugin,
@ -40,6 +42,11 @@ export const usePluginTaskStatus = () => {
})
handleRefetch()
}, [mutateAsync, handleRefetch])
const handleStopAllPlugins = useCallback(async () => {
await mutateStopAllAsync()
handleRefetch()
}, [mutateStopAllAsync, handleRefetch])
const totalPluginsLength = allPlugins.length
const runningPluginsLength = runningPlugins.length
const errorPluginsLength = errorPlugins.length
@ -65,5 +72,6 @@ export const usePluginTaskStatus = () => {
isSuccess,
isFailed,
handleClearErrorPlugin,
handleStopAllPlugins,
}
}

View File

@ -31,6 +31,7 @@ const PluginTasks = () => {
isSuccess,
isFailed,
handleClearErrorPlugin,
handleStopAllPlugins,
} = usePluginTaskStatus()
const { getIconUrl } = useGetIcon()
const canOpenMenu = isFailed || isInstalling || isInstallingWithSuccess || isInstallingWithError || isSuccess
@ -81,6 +82,11 @@ const PluginTasks = () => {
[clearPluginsAndClose, errorPlugins],
)
const handleStopAll = useCallback(async () => {
await handleStopAllPlugins()
setOpen(false)
}, [handleStopAllPlugins])
const handleClearSingle = useCallback(
(taskId: string, pluginId: string) => clearPluginsAndClose([{ taskId, plugin_unique_identifier: pluginId }]),
[clearPluginsAndClose],
@ -127,6 +133,7 @@ const PluginTasks = () => {
onClearAll={handleClearAll}
onClearErrors={handleClearErrors}
onClearSingle={handleClearSingle}
onStopAll={handleStopAll}
/>
</DropdownMenuContent>
</DropdownMenu>

View File

@ -248,6 +248,7 @@
"task.installingWithError": "Installing {{installingLength}} plugins, {{successLength}} success, {{errorLength}} failed",
"task.installingWithSuccess": "Installing {{installingLength}} plugins, {{successLength}} success.",
"task.runningPlugins": "Installing Plugins",
"task.stopAll": "Stop all",
"task.successPlugins": "Successfully Installed Plugins",
"upgrade.close": "Close",
"upgrade.description": "About to install the following plugin",

View File

@ -248,6 +248,7 @@
"task.installingWithError": "{{installingLength}} 个插件安装中,{{successLength}} 安装成功,{{errorLength}} 安装失败",
"task.installingWithSuccess": "{{installingLength}} 个插件安装中,{{successLength}} 安装成功",
"task.runningPlugins": "正在安装的插件",
"task.stopAll": "停止所有",
"task.successPlugins": "安装成功的插件",
"upgrade.close": "关闭",
"upgrade.description": "即将安装以下插件",

View File

@ -248,6 +248,7 @@
"task.installingWithError": "安裝 {{installingLength}} 個插件,{{successLength}} 成功,{{errorLength}} 失敗",
"task.installingWithSuccess": "安裝 {{installingLength}} 個插件,{{successLength}} 成功。",
"task.runningPlugins": "Installing Plugins",
"task.stopAll": "停止所有",
"task.successPlugins": "Successfully Installed Plugins",
"upgrade.close": "關閉",
"upgrade.description": "即將安裝以下插件",

View File

@ -560,6 +560,14 @@ export const useMutationClearTaskPlugin = () => {
})
}
export const useMutationStopAllTaskPlugins = () => {
return useMutation({
mutationFn: () => {
return post<{ success: boolean }>('/workspaces/current/plugin/tasks/delete_all')
},
})
}
export const usePluginManifestInfo = (pluginUID: string) => {
return useQuery({
enabled: !!pluginUID,