refactor(web): replace query option tunneling with queryOptions factories

Extract sandboxFilesTreeOptions and buildTreeFromFlatList from
useSandboxFilesTree so callers that need custom TanStack Query behavior
(e.g. refetchInterval) can compose at the call site instead of tunneling
options through the hook. Remove the thin useGetSandboxProviderList
wrapper in favor of inline oRPC queryOptions in the component.

Also remove redundant .input(type<unknown>()) from three no-input GET
contracts—oc already defaults TInputSchema to Schema<unknown, unknown>.
This commit is contained in:
yyh
2026-02-27 11:58:16 +08:00
parent 60b02e6d2b
commit 0bdd21bc17
9 changed files with 44 additions and 74 deletions

View File

@ -1,11 +1,12 @@
'use client'
import type { SandboxProvider } from '@/types/sandbox-provider'
import { useQuery } from '@tanstack/react-query'
import { memo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Loading from '@/app/components/base/loading'
import { useAppContext } from '@/context/app-context'
import { useGetSandboxProviderList } from '@/service/use-sandbox-provider'
import { consoleQuery } from '@/service/client'
import ConfigModal from './config-modal'
import ProviderCard from './provider-card'
import SwitchModal from './switch-modal'
@ -13,7 +14,7 @@ import SwitchModal from './switch-modal'
const SandboxProviderPage = () => {
const { t } = useTranslation()
const { isCurrentWorkspaceManager, isLoadingCurrentWorkspace } = useAppContext()
const { data: providers, isLoading } = useGetSandboxProviderList()
const { data: providers, isLoading } = useQuery(consoleQuery.sandboxProvider.getSandboxProviderList.queryOptions())
const [configModalProvider, setConfigModalProvider] = useState<SandboxProvider | null>(null)
const [switchModalProvider, setSwitchModalProvider] = useState<SandboxProvider | null>(null)

View File

@ -1,4 +1,4 @@
import type { SandboxFileNode, SandboxFileTreeNode } from '@/types/sandbox-file'
import type { SandboxFileNode } from '@/types/sandbox-file'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import ArtifactsTab from './artifacts-tab'
import { InspectTab } from './types'
@ -21,9 +21,7 @@ const mocks = vi.hoisted(() => ({
isResponding: false,
bottomPanelWidth: 640,
} as MockStoreState,
treeData: undefined as SandboxFileTreeNode[] | undefined,
flatData: [] as SandboxFileNode[],
hasFiles: false,
isLoading: false,
fetchDownloadUrl: vi.fn(),
mockUseQuery: vi.fn(),
@ -31,6 +29,10 @@ const mocks = vi.hoisted(() => ({
queryKey: ['sandboxFile', 'downloadFile'],
queryFn: vi.fn(),
}),
mockTreeOptions: vi.fn().mockReturnValue({
queryKey: ['sandboxFile', 'listFiles'],
queryFn: vi.fn(),
}),
}))
vi.mock('../store', () => ({
@ -42,14 +44,10 @@ vi.mock('@tanstack/react-query', async importOriginal => ({
useQuery: (options: unknown) => mocks.mockUseQuery(options),
}))
vi.mock('@/service/use-sandbox-file', () => ({
vi.mock('@/service/use-sandbox-file', async importOriginal => ({
...(await importOriginal<typeof import('@/service/use-sandbox-file')>()),
sandboxFileDownloadUrlOptions: (...args: unknown[]) => mocks.mockDownloadUrlOptions(...args),
useSandboxFilesTree: () => ({
data: mocks.treeData,
flatData: mocks.flatData,
hasFiles: mocks.hasFiles,
isLoading: mocks.isLoading,
}),
sandboxFilesTreeOptions: (...args: unknown[]) => mocks.mockTreeOptions(...args),
useDownloadSandboxFile: () => ({
mutateAsync: mocks.fetchDownloadUrl,
isPending: false,
@ -74,18 +72,6 @@ vi.mock('@/utils/download', () => ({
downloadUrl: vi.fn(),
}))
const createTreeFileNode = (overrides: Partial<SandboxFileTreeNode> = {}): SandboxFileTreeNode => ({
id: 'a.txt',
name: 'a.txt',
path: 'a.txt',
node_type: 'file',
size: 128,
mtime: 1700000000,
extension: 'txt',
children: [],
...overrides,
})
const createFlatFileNode = (overrides: Partial<SandboxFileNode> = {}): SandboxFileNode => ({
path: 'a.txt',
is_dir: false,
@ -103,13 +89,20 @@ describe('ArtifactsTab', () => {
mocks.storeState.isResponding = false
mocks.storeState.bottomPanelWidth = 640
mocks.treeData = [createTreeFileNode()]
mocks.flatData = [createFlatFileNode()]
mocks.hasFiles = true
mocks.isLoading = false
mocks.mockUseQuery.mockReturnValue({
data: undefined,
isLoading: false,
mocks.mockUseQuery.mockImplementation((options: { queryKey?: unknown }) => {
const treeKey = mocks.mockTreeOptions.mock.results.at(-1)?.value?.queryKey
if (treeKey && options.queryKey === treeKey) {
return {
data: mocks.flatData,
isLoading: mocks.isLoading,
}
}
return {
data: undefined,
isLoading: false,
}
})
})
@ -128,9 +121,7 @@ describe('ArtifactsTab', () => {
expect(mocks.mockDownloadUrlOptions).toHaveBeenCalledWith('app-1', 'a.txt')
})
mocks.treeData = undefined
mocks.flatData = []
mocks.hasFiles = false
rerender(<ArtifactsTab {...headerProps} />)

View File

@ -11,7 +11,7 @@ import Loading from '@/app/components/base/loading'
import ArtifactsTree from '@/app/components/workflow/skill/file-tree/artifacts/artifacts-tree'
import ReadOnlyFilePreview from '@/app/components/workflow/skill/viewer/read-only-file-preview'
import { useDocLink } from '@/context/i18n'
import { sandboxFileDownloadUrlOptions, useDownloadSandboxFile, useSandboxFilesTree } from '@/service/use-sandbox-file'
import { buildTreeFromFlatList, sandboxFileDownloadUrlOptions, sandboxFilesTreeOptions, useDownloadSandboxFile } from '@/service/use-sandbox-file'
import { cn } from '@/utils/classnames'
import { downloadUrl } from '@/utils/download'
import { useStore } from '../store'
@ -67,10 +67,12 @@ const ArtifactsTab = (headerProps: InspectHeaderProps) => {
)
const isResponding = useStore(s => s.isResponding)
const { data: treeData, flatData, hasFiles, isLoading } = useSandboxFilesTree(appId, {
enabled: !!appId,
const { data: flatData, isLoading } = useQuery({
...sandboxFilesTreeOptions(appId),
refetchInterval: (isWorkflowRunning || isResponding) ? 5000 : false,
})
const treeData = useMemo(() => flatData ? buildTreeFromFlatList(flatData) : undefined, [flatData])
const hasFiles = (flatData?.length ?? 0) > 0
const { mutateAsync: fetchDownloadUrl, isPending: isDownloading } = useDownloadSandboxFile(appId)
const [selectedFile, setSelectedFile] = useState<SandboxFileTreeNode | null>(null)
const selectedFilePath = useMemo(() => {

View File

@ -1,9 +1,10 @@
import type { FC } from 'react'
import { useQuery } from '@tanstack/react-query'
import { useCallback, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import Button from '@/app/components/base/button'
import { useFeatures } from '@/app/components/base/features/hooks'
import { useSandboxFilesTree } from '@/service/use-sandbox-file'
import { sandboxFilesTreeOptions } from '@/service/use-sandbox-file'
import useCurrentVars from '../hooks/use-inspect-vars-crud'
import { useStore } from '../store'
import ArtifactsTab from './artifacts-tab'
@ -28,9 +29,8 @@ const VariablesPanel: FC<{ onClose: () => void }> = ({ onClose }) => {
return [...environmentVariables, ...conversationVars, ...systemVars, ...nodesWithInspectVars].length === 0
}, [environmentVariables, conversationVars, systemVars, nodesWithInspectVars])
const { hasFiles: hasArtifacts } = useSandboxFilesTree(appId, {
enabled: !!appId && sandboxEnabled,
})
const { data: sandboxFiles } = useQuery(sandboxFilesTreeOptions(sandboxEnabled ? appId : undefined))
const hasArtifacts = (sandboxFiles?.length ?? 0) > 0
const handleClear = useCallback(() => {
deleteAllInspectorVars()

View File

@ -6,7 +6,6 @@ export const invoicesContract = base
path: '/billing/invoices',
method: 'GET',
})
.input(type<unknown>())
.output(type<{ url: string }>())
export const bindPartnerStackContract = base

View File

@ -7,7 +7,6 @@ export const getSandboxProviderListContract = base
path: '/workspaces/current/sandbox-providers',
method: 'GET',
})
.input(type<unknown>())
.output(type<SandboxProvider[]>())
export const saveSandboxProviderConfigContract = base

View File

@ -7,5 +7,4 @@ export const systemFeaturesContract = base
path: '/system-features',
method: 'GET',
})
.input(type<unknown>())
.output(type<SystemFeatures>())

View File

@ -14,6 +14,14 @@ export function sandboxFileDownloadUrlOptions(appId: string | undefined, path: s
})
}
export function sandboxFilesTreeOptions(appId: string | undefined) {
return consoleQuery.sandboxFile.listFiles.queryOptions({
input: appId
? { params: { appId }, query: { recursive: true } }
: skipToken,
})
}
type InvalidateSandboxFilesOptions = {
refetchDownloadFile?: boolean
}
@ -47,7 +55,7 @@ export function useDownloadSandboxFile(appId: string | undefined) {
})
}
function buildTreeFromFlatList(nodes: SandboxFileNode[]): SandboxFileTreeNode[] {
export function buildTreeFromFlatList(nodes: SandboxFileNode[]): SandboxFileTreeNode[] {
const nodeMap = new Map<string, SandboxFileTreeNode>()
const roots: SandboxFileTreeNode[] = []
@ -86,25 +94,8 @@ function buildTreeFromFlatList(nodes: SandboxFileNode[]): SandboxFileTreeNode[]
return roots
}
type UseSandboxFilesTreeOptions = {
enabled?: boolean
refetchInterval?: number | false
}
export function useSandboxFilesTree(
appId: string | undefined,
options?: UseSandboxFilesTreeOptions,
) {
const input = appId && (options?.enabled ?? true)
? { params: { appId }, query: { recursive: true } }
: skipToken
const { data, isLoading, error } = useQuery({
...consoleQuery.sandboxFile.listFiles.queryOptions({
input,
}),
refetchInterval: options?.refetchInterval,
})
export function useSandboxFilesTree(appId: string | undefined) {
const { data, isLoading, error } = useQuery(sandboxFilesTreeOptions(appId))
const treeData = useMemo(() => {
if (!data)
@ -112,14 +103,10 @@ export function useSandboxFilesTree(
return buildTreeFromFlatList(data)
}, [data])
const hasFiles = useMemo(() => {
return (data?.length ?? 0) > 0
}, [data])
return {
data: treeData,
flatData: data,
hasFiles,
hasFiles: (data?.length ?? 0) > 0,
isLoading,
error,
}

View File

@ -1,17 +1,9 @@
import {
useMutation,
useQuery,
useQueryClient,
} from '@tanstack/react-query'
import { consoleClient, consoleQuery } from '@/service/client'
export const useGetSandboxProviderList = () => {
return useQuery({
queryKey: consoleQuery.sandboxProvider.getSandboxProviderList.queryKey(),
queryFn: () => consoleClient.sandboxProvider.getSandboxProviderList(),
})
}
export const useSaveSandboxProviderConfig = () => {
const queryClient = useQueryClient()
return useMutation({