feat(web): app switch api

This commit is contained in:
JzoNg 2026-04-03 13:56:00 +08:00
parent 3ac4caf735
commit 61e257b2a8
6 changed files with 151 additions and 1 deletions

View File

@ -1,3 +1,4 @@
import type { WorkflowTypeConversionTarget } from '@/types/workflow'
import { type } from '@orpc/contract'
import { base } from '../base'
@ -12,3 +13,18 @@ export const appDeleteContract = base
}
}>())
.output(type<unknown>())
export const appWorkflowTypeConvertContract = base
.route({
path: '/apps/{appId}/workflows/convert-type',
method: 'POST',
})
.input(type<{
params: {
appId: string
}
query: {
target_type: WorkflowTypeConversionTarget
}
}>())
.output(type<unknown>())

View File

@ -1,5 +1,5 @@
import type { InferContractRouterInputs } from '@orpc/contract'
import { appDeleteContract } from './console/apps'
import { appDeleteContract, appWorkflowTypeConvertContract } from './console/apps'
import { bindPartnerStackContract, invoicesContract } from './console/billing'
import {
availableEvaluationMetricsContract,
@ -98,6 +98,7 @@ export const consoleRouterContract = {
systemFeatures: systemFeaturesContract,
apps: {
deleteApp: appDeleteContract,
convertWorkflowType: appWorkflowTypeConvertContract,
},
explore: {
apps: exploreAppsContract,

View File

@ -0,0 +1,96 @@
import type { ReactNode } from 'react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { act, renderHook, waitFor } from '@testing-library/react'
import { useConvertWorkflowTypeMutation } from '../use-apps'
const {
invalidateQueries,
convertWorkflowTypeMutationFn,
convertWorkflowTypeMutationOptions,
} = vi.hoisted(() => ({
invalidateQueries: vi.fn(),
convertWorkflowTypeMutationFn: vi.fn(),
convertWorkflowTypeMutationOptions: vi.fn(),
}))
vi.mock('@tanstack/react-query', async (importOriginal) => {
const actual = await importOriginal<typeof import('@tanstack/react-query')>()
return {
...actual,
useQueryClient: () => ({
invalidateQueries,
}),
}
})
vi.mock('@/service/client', () => ({
consoleClient: {},
consoleQuery: {
apps: {
convertWorkflowType: {
mutationOptions: convertWorkflowTypeMutationOptions,
},
},
},
}))
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
})
return ({ children }: { children: ReactNode }) => (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
)
}
// Scenario: workflow type conversion forwards the expected API input and refreshes app caches.
describe('useConvertWorkflowTypeMutation', () => {
beforeEach(() => {
vi.clearAllMocks()
convertWorkflowTypeMutationFn.mockResolvedValue({})
convertWorkflowTypeMutationOptions.mockImplementation(options => ({
mutationFn: convertWorkflowTypeMutationFn,
onSuccess: options?.onSuccess,
}))
})
it('should convert workflow type and invalidate app queries when mutation succeeds', async () => {
// Arrange
const { result } = renderHook(() => useConvertWorkflowTypeMutation(), {
wrapper: createWrapper(),
})
// Act
await act(async () => {
result.current.mutate({
params: { appId: 'app-1' },
query: { target_type: 'evaluation' },
})
})
// Assert
await waitFor(() => {
expect(convertWorkflowTypeMutationFn).toHaveBeenCalledWith({
params: { appId: 'app-1' },
query: { target_type: 'evaluation' },
})
})
await waitFor(() => {
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['apps', 'detail', 'app-1'],
})
})
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['apps', 'list'],
})
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: ['apps', 'full-list'],
})
})
})

View File

@ -11,6 +11,7 @@ import type {
WorkflowDailyConversationsResponse,
} from '@/models/app'
import type { App } from '@/types/app'
import type { WorkflowTypeConversionTarget } from '@/types/workflow'
import {
keepPreviousData,
useInfiniteQuery,
@ -148,6 +149,30 @@ export const useDeleteAppMutation = () => {
})
}
export const useConvertWorkflowTypeMutation = () => {
const queryClient = useQueryClient()
return useMutation({
...consoleQuery.apps.convertWorkflowType.mutationOptions({
onSuccess: async (_, variables) => {
await Promise.all([
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'detail', variables.params.appId],
}),
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'list'],
}),
queryClient.invalidateQueries({
queryKey: useAppFullListKey,
}),
])
},
}),
})
}
export type { WorkflowTypeConversionTarget }
const useAppStatisticsQuery = <T>(metric: string, appId: string, params?: DateRangeParams) => {
return useQuery<T>({
queryKey: [NAME_SPACE, 'statistics', metric, appId, params],

View File

@ -47,6 +47,14 @@ export enum AppModeEnum {
}
export const AppModes = [AppModeEnum.COMPLETION, AppModeEnum.WORKFLOW, AppModeEnum.CHAT, AppModeEnum.ADVANCED_CHAT, AppModeEnum.AGENT_CHAT] as const
export enum AppTypeEnum {
WORKFLOW = 'workflow',
CHAT = 'chat',
RAG_PIPELINE = 'rag-pipeline',
SNIPPET = 'snippet',
EVALUATION = 'evaluation',
}
/**
* Variable type
*/
@ -325,6 +333,8 @@ export type App = {
/** Mode */
mode: AppModeEnum
/** Type */
type?: AppTypeEnum
/** Enable web app */
enable_site: boolean
/** Enable web API */

View File

@ -427,6 +427,8 @@ export type PublishWorkflowParams = {
releaseNotes: string
}
export type WorkflowTypeConversionTarget = 'workflow' | 'evaluation'
export type UpdateWorkflowParams = {
url: string
title: string