mirror of https://github.com/langgenius/dify.git
refactor goto-anything scopes and registry
This commit is contained in:
parent
e5a98b10a9
commit
1367497deb
|
|
@ -9,7 +9,7 @@ import type { MockedFunction } from 'vitest'
|
|||
* 4. Ensure errors don't propagate to UI layer causing "search failed"
|
||||
*/
|
||||
|
||||
import { Actions, searchAnything } from '@/app/components/goto-anything/actions'
|
||||
import { appScope, knowledgeScope, pluginScope, searchAnything } from '@/app/components/goto-anything/actions'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { postMarketplace } from '@/service/base'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
|
|
@ -30,6 +30,7 @@ vi.mock('@/service/datasets', () => ({
|
|||
const mockPostMarketplace = postMarketplace as MockedFunction<typeof postMarketplace>
|
||||
const mockFetchAppList = fetchAppList as MockedFunction<typeof fetchAppList>
|
||||
const mockFetchDatasets = fetchDatasets as MockedFunction<typeof fetchDatasets>
|
||||
const searchScopes = [appScope, knowledgeScope, pluginScope]
|
||||
|
||||
describe('GotoAnything Search Error Handling', () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -49,10 +50,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
// Mock marketplace API failure (403 permission denied)
|
||||
mockPostMarketplace.mockRejectedValue(new Error('HTTP 403: Forbidden'))
|
||||
|
||||
const pluginAction = Actions.plugin
|
||||
|
||||
// Directly call plugin action's search method
|
||||
const result = await pluginAction.search('@plugin', 'test', 'en')
|
||||
const result = await pluginScope.search('@plugin', 'test', 'en')
|
||||
|
||||
// Should return empty array instead of throwing error
|
||||
expect(result).toEqual([])
|
||||
|
|
@ -72,8 +70,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
data: { plugins: [] },
|
||||
})
|
||||
|
||||
const pluginAction = Actions.plugin
|
||||
const result = await pluginAction.search('@plugin', '', 'en')
|
||||
const result = await pluginScope.search('@plugin', '', 'en')
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
|
@ -84,8 +81,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
data: null,
|
||||
})
|
||||
|
||||
const pluginAction = Actions.plugin
|
||||
const result = await pluginAction.search('@plugin', 'test', 'en')
|
||||
const result = await pluginScope.search('@plugin', 'test', 'en')
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
|
@ -96,8 +92,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
// Mock app API failure
|
||||
mockFetchAppList.mockRejectedValue(new Error('API Error'))
|
||||
|
||||
const appAction = Actions.app
|
||||
const result = await appAction.search('@app', 'test', 'en')
|
||||
const result = await appScope.search('@app', 'test', 'en')
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
|
@ -106,8 +101,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
// Mock knowledge API failure
|
||||
mockFetchDatasets.mockRejectedValue(new Error('API Error'))
|
||||
|
||||
const knowledgeAction = Actions.knowledge
|
||||
const result = await knowledgeAction.search('@knowledge', 'test', 'en')
|
||||
const result = await knowledgeScope.search('@knowledge', 'test', 'en')
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
|
@ -120,7 +114,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
mockFetchDatasets.mockResolvedValue({ data: [], has_more: false, limit: 10, page: 1, total: 0 })
|
||||
mockPostMarketplace.mockRejectedValue(new Error('Plugin API failed'))
|
||||
|
||||
const result = await searchAnything('en', 'test')
|
||||
const result = await searchAnything('en', 'test', undefined, searchScopes)
|
||||
|
||||
// Should return successful results even if plugin search fails
|
||||
expect(result).toEqual([])
|
||||
|
|
@ -131,8 +125,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
// Mock plugin API failure
|
||||
mockPostMarketplace.mockRejectedValue(new Error('Plugin service unavailable'))
|
||||
|
||||
const pluginAction = Actions.plugin
|
||||
const result = await searchAnything('en', '@plugin test', pluginAction)
|
||||
const result = await searchAnything('en', '@plugin test', pluginScope, searchScopes)
|
||||
|
||||
// Should return empty array instead of throwing error
|
||||
expect(result).toEqual([])
|
||||
|
|
@ -142,8 +135,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
// Mock app API failure
|
||||
mockFetchAppList.mockRejectedValue(new Error('App service unavailable'))
|
||||
|
||||
const appAction = Actions.app
|
||||
const result = await searchAnything('en', '@app test', appAction)
|
||||
const result = await searchAnything('en', '@app test', appScope, searchScopes)
|
||||
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
|
@ -157,9 +149,9 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
mockFetchDatasets.mockRejectedValue(new Error('Dataset API failed'))
|
||||
|
||||
const actions = [
|
||||
{ name: '@plugin', action: Actions.plugin },
|
||||
{ name: '@app', action: Actions.app },
|
||||
{ name: '@knowledge', action: Actions.knowledge },
|
||||
{ name: '@plugin', action: pluginScope },
|
||||
{ name: '@app', action: appScope },
|
||||
{ name: '@knowledge', action: knowledgeScope },
|
||||
]
|
||||
|
||||
for (const { name, action } of actions) {
|
||||
|
|
@ -173,7 +165,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
it('empty search term should be handled properly', async () => {
|
||||
mockPostMarketplace.mockResolvedValue({ data: { plugins: [] } })
|
||||
|
||||
const result = await searchAnything('en', '@plugin ', Actions.plugin)
|
||||
const result = await searchAnything('en', '@plugin ', pluginScope, searchScopes)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
|
|
@ -183,7 +175,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
|
||||
mockPostMarketplace.mockRejectedValue(timeoutError)
|
||||
|
||||
const result = await searchAnything('en', '@plugin test', Actions.plugin)
|
||||
const result = await searchAnything('en', '@plugin test', pluginScope, searchScopes)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
|
|
@ -191,7 +183,7 @@ describe('GotoAnything Search Error Handling', () => {
|
|||
const parseError = new SyntaxError('Unexpected token in JSON')
|
||||
mockPostMarketplace.mockRejectedValue(parseError)
|
||||
|
||||
const result = await searchAnything('en', '@plugin test', Actions.plugin)
|
||||
const result = await searchAnything('en', '@plugin test', pluginScope, searchScopes)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ActionItem, AppSearchResult } from './types'
|
||||
import type { AppSearchResult, ScopeDescriptor } from './types'
|
||||
import type { App } from '@/types/app'
|
||||
import { fetchAppList } from '@/service/apps'
|
||||
import { getRedirectionPath } from '@/utils/app-redirection'
|
||||
|
|
@ -36,8 +36,8 @@ const parser = (apps: App[]): AppSearchResult[] => {
|
|||
}))
|
||||
}
|
||||
|
||||
export const appAction: ActionItem = {
|
||||
key: ACTION_KEYS.APP,
|
||||
export const appScope: ScopeDescriptor = {
|
||||
id: 'app',
|
||||
shortcut: ACTION_KEYS.APP,
|
||||
title: 'Search Applications',
|
||||
description: 'Search and navigate to your applications',
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ export {
|
|||
export { slashCommandRegistry, SlashCommandRegistry } from './registry'
|
||||
|
||||
// Command system exports
|
||||
export { slashAction } from './slash'
|
||||
export { slashScope } from './slash'
|
||||
export { registerSlashCommands, SlashCommandProvider, unregisterSlashCommands } from './slash'
|
||||
|
||||
export type { SlashCommandHandler } from './types'
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
'use client'
|
||||
import type { ActionItem } from '../types'
|
||||
import type { ScopeDescriptor } from '../types'
|
||||
import type { SlashCommandDependencies } from './types'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { useEffect } from 'react'
|
||||
|
|
@ -8,7 +8,6 @@ import i18n from '@/i18n-config/i18next-config'
|
|||
import { ACTION_KEYS } from '../../constants'
|
||||
import { accountCommand } from './account'
|
||||
import { bananaCommand } from './banana'
|
||||
import { executeCommand } from './command-bus'
|
||||
import { communityCommand } from './community'
|
||||
import { docsCommand } from './docs'
|
||||
import { forumCommand } from './forum'
|
||||
|
|
@ -17,17 +16,11 @@ import { slashCommandRegistry } from './registry'
|
|||
import { themeCommand } from './theme'
|
||||
import { zenCommand } from './zen'
|
||||
|
||||
export const slashAction: ActionItem = {
|
||||
key: ACTION_KEYS.SLASH,
|
||||
export const slashScope: ScopeDescriptor = {
|
||||
id: 'slash',
|
||||
shortcut: ACTION_KEYS.SLASH,
|
||||
title: i18n.t('app.gotoAnything.actions.slashTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.slashDesc'),
|
||||
action: (result) => {
|
||||
if (result.type !== 'command')
|
||||
return
|
||||
const { command, args } = result.data
|
||||
executeCommand(command, args)
|
||||
},
|
||||
search: async (query, _searchTerm = '') => {
|
||||
// Delegate all search logic to the command registry system
|
||||
return slashCommandRegistry.search(query, i18n.language)
|
||||
|
|
|
|||
|
|
@ -5,96 +5,64 @@
|
|||
* Actions handle different types of searches: apps, knowledge bases, plugins, workflow nodes, and commands.
|
||||
*/
|
||||
|
||||
import type { ActionItem, ScopeDescriptor, SearchResult } from './types'
|
||||
import type { ScopeContext, ScopeDescriptor, SearchResult } from './types'
|
||||
import { ACTION_KEYS } from '../constants'
|
||||
import { appAction } from './app'
|
||||
import { slashAction } from './commands'
|
||||
import { appScope } from './app'
|
||||
import { slashScope } from './commands'
|
||||
import { slashCommandRegistry } from './commands/registry'
|
||||
import { knowledgeAction } from './knowledge'
|
||||
import { pluginAction } from './plugin'
|
||||
import { scopeRegistry } from './scope-registry'
|
||||
import { knowledgeScope } from './knowledge'
|
||||
import { pluginScope } from './plugin'
|
||||
import { registerRagPipelineNodeScope } from './rag-pipeline-nodes'
|
||||
import { scopeRegistry, useScopeRegistry } from './scope-registry'
|
||||
import { registerWorkflowNodeScope } from './workflow-nodes'
|
||||
|
||||
let defaultScopesRegistered = false
|
||||
let scopesInitialized = false
|
||||
|
||||
export const registerDefaultScopes = () => {
|
||||
if (defaultScopesRegistered)
|
||||
export const initGotoAnythingScopes = () => {
|
||||
if (scopesInitialized)
|
||||
return
|
||||
|
||||
defaultScopesRegistered = true
|
||||
scopesInitialized = true
|
||||
|
||||
scopeRegistry.register({
|
||||
id: 'slash',
|
||||
shortcut: ACTION_KEYS.SLASH,
|
||||
title: 'Commands',
|
||||
description: 'Execute commands',
|
||||
search: slashAction.search,
|
||||
isAvailable: () => true,
|
||||
})
|
||||
|
||||
scopeRegistry.register({
|
||||
id: 'app',
|
||||
shortcut: ACTION_KEYS.APP,
|
||||
title: 'Search Applications',
|
||||
description: 'Search and navigate to your applications',
|
||||
search: appAction.search,
|
||||
isAvailable: () => true,
|
||||
})
|
||||
|
||||
scopeRegistry.register({
|
||||
id: 'knowledge',
|
||||
shortcut: ACTION_KEYS.KNOWLEDGE,
|
||||
title: 'Search Knowledge Bases',
|
||||
description: 'Search and navigate to your knowledge bases',
|
||||
search: knowledgeAction.search,
|
||||
isAvailable: () => true,
|
||||
})
|
||||
|
||||
scopeRegistry.register({
|
||||
id: 'plugin',
|
||||
shortcut: ACTION_KEYS.PLUGIN,
|
||||
title: 'Search Plugins',
|
||||
description: 'Search and navigate to your plugins',
|
||||
search: pluginAction.search,
|
||||
isAvailable: () => true,
|
||||
})
|
||||
scopeRegistry.register(slashScope)
|
||||
scopeRegistry.register(appScope)
|
||||
scopeRegistry.register(knowledgeScope)
|
||||
scopeRegistry.register(pluginScope)
|
||||
registerWorkflowNodeScope()
|
||||
registerRagPipelineNodeScope()
|
||||
}
|
||||
|
||||
// Legacy export for backward compatibility
|
||||
export const Actions = {
|
||||
slash: slashAction,
|
||||
app: appAction,
|
||||
knowledge: knowledgeAction,
|
||||
plugin: pluginAction,
|
||||
export const useGotoAnythingScopes = (context: ScopeContext) => {
|
||||
initGotoAnythingScopes()
|
||||
return useScopeRegistry(context)
|
||||
}
|
||||
|
||||
const getScopeId = (scope: ScopeDescriptor | ActionItem) => ('id' in scope ? scope.id : scope.key)
|
||||
const isSlashScope = (scope: ScopeDescriptor) => {
|
||||
if (scope.shortcut === ACTION_KEYS.SLASH)
|
||||
return true
|
||||
return scope.aliases?.includes(ACTION_KEYS.SLASH) ?? false
|
||||
}
|
||||
|
||||
const isSlashScope = (scope: ScopeDescriptor | ActionItem) => scope.shortcut === ACTION_KEYS.SLASH
|
||||
const getScopeShortcuts = (scope: ScopeDescriptor) => [scope.shortcut, ...(scope.aliases ?? [])]
|
||||
|
||||
export const searchAnything = async (
|
||||
locale: string,
|
||||
query: string,
|
||||
scope?: ScopeDescriptor | ActionItem,
|
||||
scopes?: (ScopeDescriptor | ActionItem)[],
|
||||
scope: ScopeDescriptor | undefined,
|
||||
scopes: ScopeDescriptor[],
|
||||
): Promise<SearchResult[]> => {
|
||||
registerDefaultScopes()
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
// Backwards compatibility: if scopes is not provided or empty, use non-page-specific scopes
|
||||
const effectiveScopes = (scopes && scopes.length > 0)
|
||||
? scopes
|
||||
: scopeRegistry.getScopes({ isWorkflowPage: false, isRagPipelinePage: false })
|
||||
|
||||
if (scope) {
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
const scopeId = getScopeId(scope)
|
||||
const prefixPattern = new RegExp(`^(${escapeRegExp(scope.shortcut)})\\s*`)
|
||||
const shortcuts = getScopeShortcuts(scope).map(escapeRegExp)
|
||||
const prefixPattern = new RegExp(`^(${shortcuts.join('|')})\\s*`)
|
||||
const searchTerm = trimmedQuery.replace(prefixPattern, '').trim()
|
||||
try {
|
||||
return await scope.search(query, searchTerm, locale)
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Search failed for ${scopeId}:`, error)
|
||||
console.warn(`Search failed for ${scope.id}:`, error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
|
@ -103,11 +71,11 @@ export const searchAnything = async (
|
|||
return []
|
||||
|
||||
// Filter out slash commands from general search
|
||||
const searchScopes = effectiveScopes.filter(scope => !isSlashScope(scope))
|
||||
const searchScopes = scopes.filter(scope => !isSlashScope(scope))
|
||||
|
||||
// Use Promise.allSettled to handle partial failures gracefully
|
||||
const searchPromises = searchScopes.map(async (action) => {
|
||||
const actionId = getScopeId(action)
|
||||
const actionId = action.id
|
||||
try {
|
||||
const results = await action.search(query, query, locale)
|
||||
return { success: true, data: results, actionType: actionId }
|
||||
|
|
@ -128,7 +96,7 @@ export const searchAnything = async (
|
|||
allResults.push(...result.value.data)
|
||||
}
|
||||
else {
|
||||
const actionKey = getScopeId(searchScopes[index]) || 'unknown'
|
||||
const actionKey = searchScopes[index]?.id || 'unknown'
|
||||
failedActions.push(actionKey)
|
||||
}
|
||||
})
|
||||
|
|
@ -142,11 +110,10 @@ export const searchAnything = async (
|
|||
// ...
|
||||
|
||||
export const matchAction = (query: string, scopes: ScopeDescriptor[]) => {
|
||||
registerDefaultScopes()
|
||||
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
return scopes.find((scope) => {
|
||||
// Special handling for slash commands
|
||||
if (scope.shortcut === ACTION_KEYS.SLASH) {
|
||||
if (isSlashScope(scope)) {
|
||||
const allCommands = slashCommandRegistry.getAllCommands()
|
||||
return allCommands.some((cmd) => {
|
||||
const cmdPattern = `/${cmd.name}`
|
||||
|
|
@ -158,7 +125,8 @@ export const matchAction = (query: string, scopes: ScopeDescriptor[]) => {
|
|||
|
||||
// Check if query matches shortcut (exact or prefix)
|
||||
// Only match if it's the full shortcut followed by space
|
||||
const reg = new RegExp(`^(${escapeRegExp(scope.shortcut)})(?:\\s|$)`)
|
||||
const shortcuts = getScopeShortcuts(scope).map(escapeRegExp)
|
||||
const reg = new RegExp(`^(${shortcuts.join('|')})(?:\\s|$)`)
|
||||
return reg.test(query)
|
||||
})
|
||||
}
|
||||
|
|
@ -166,4 +134,4 @@ export const matchAction = (query: string, scopes: ScopeDescriptor[]) => {
|
|||
export * from './commands'
|
||||
export * from './scope-registry'
|
||||
export * from './types'
|
||||
export { appAction, knowledgeAction, pluginAction }
|
||||
export { appScope, knowledgeScope, pluginScope }
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { ActionItem, KnowledgeSearchResult } from './types'
|
||||
import type { KnowledgeSearchResult, ScopeDescriptor } from './types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import { fetchDatasets } from '@/service/datasets'
|
||||
import { cn } from '@/utils/classnames'
|
||||
|
|
@ -31,9 +31,10 @@ const parser = (datasets: DataSet[]): KnowledgeSearchResult[] => {
|
|||
})
|
||||
}
|
||||
|
||||
export const knowledgeAction: ActionItem = {
|
||||
key: ACTION_KEYS.KNOWLEDGE,
|
||||
shortcut: '@kb',
|
||||
export const knowledgeScope: ScopeDescriptor = {
|
||||
id: 'knowledge',
|
||||
shortcut: ACTION_KEYS.KNOWLEDGE,
|
||||
aliases: ['@kb'],
|
||||
title: 'Search Knowledge Bases',
|
||||
description: 'Search and navigate to your knowledge bases',
|
||||
// action,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { Plugin, PluginsFromMarketplaceResponse } from '../../plugins/types'
|
||||
import type { ActionItem, PluginSearchResult } from './types'
|
||||
import type { PluginSearchResult, ScopeDescriptor } from './types'
|
||||
import { renderI18nObject } from '@/i18n-config'
|
||||
import { postMarketplace } from '@/service/base'
|
||||
import Icon from '../../plugins/card/base/card-icon'
|
||||
|
|
@ -19,8 +19,8 @@ const parser = (plugins: Plugin[], locale: string): PluginSearchResult[] => {
|
|||
})
|
||||
}
|
||||
|
||||
export const pluginAction: ActionItem = {
|
||||
key: ACTION_KEYS.PLUGIN,
|
||||
export const pluginScope: ScopeDescriptor = {
|
||||
id: 'plugin',
|
||||
shortcut: ACTION_KEYS.PLUGIN,
|
||||
title: 'Search Plugins',
|
||||
description: 'Search and navigate to your plugins',
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ACTION_KEYS } from '../constants'
|
|||
import { scopeRegistry } from './scope-registry'
|
||||
|
||||
const scopeId = 'rag-pipeline-node'
|
||||
let scopeRegistered = false
|
||||
|
||||
const buildSearchHandler = (searchFn?: (searchTerm: string) => SearchResult[]): ScopeSearchHandler => {
|
||||
return async (_, searchTerm = '', _locale) => {
|
||||
|
|
@ -19,22 +20,22 @@ const buildSearchHandler = (searchFn?: (searchTerm: string) => SearchResult[]):
|
|||
}
|
||||
}
|
||||
|
||||
export const registerRagPipelineNodeScope = () => {
|
||||
if (scopeRegistered)
|
||||
return
|
||||
|
||||
scopeRegistered = true
|
||||
scopeRegistry.register({
|
||||
id: scopeId,
|
||||
shortcut: ACTION_KEYS.NODE,
|
||||
title: 'Search RAG Pipeline Nodes',
|
||||
description: 'Find and jump to nodes in the current RAG pipeline by name or type',
|
||||
isAvailable: context => context.isRagPipelinePage,
|
||||
search: buildSearchHandler(),
|
||||
})
|
||||
}
|
||||
|
||||
export const setRagPipelineNodesSearchFn = (fn: (searchTerm: string) => SearchResult[]) => {
|
||||
registerRagPipelineNodeScope()
|
||||
scopeRegistry.updateSearchHandler(scopeId, buildSearchHandler(fn))
|
||||
}
|
||||
|
||||
// Register the RAG pipeline nodes action
|
||||
scopeRegistry.register({
|
||||
id: scopeId,
|
||||
shortcut: ACTION_KEYS.NODE,
|
||||
title: 'Search RAG Pipeline Nodes',
|
||||
description: 'Find and jump to nodes in the current RAG pipeline by name or type',
|
||||
isAvailable: context => context.isRagPipelinePage,
|
||||
search: buildSearchHandler(),
|
||||
})
|
||||
|
||||
// Legacy export
|
||||
export const ragPipelineNodesAction = {
|
||||
key: ACTION_KEYS.NODE,
|
||||
search: async () => [],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,10 @@ export type ScopeDescriptor = {
|
|||
* Shortcut to trigger this scope (e.g. '@app')
|
||||
*/
|
||||
shortcut: string
|
||||
/**
|
||||
* Additional shortcuts that map to this scope (e.g. ['@kb'])
|
||||
*/
|
||||
aliases?: string[]
|
||||
/**
|
||||
* I18n key or string for the scope title
|
||||
*/
|
||||
|
|
@ -95,9 +99,7 @@ class ScopeRegistry {
|
|||
|
||||
export const scopeRegistry = new ScopeRegistry()
|
||||
|
||||
export const useScopeRegistry = (context: ScopeContext, initialize?: () => void) => {
|
||||
initialize?.()
|
||||
|
||||
export const useScopeRegistry = (context: ScopeContext) => {
|
||||
const subscribe = useCallback(
|
||||
(listener: Listener) => scopeRegistry.subscribe(listener),
|
||||
[],
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import type { ReactNode } from 'react'
|
||||
import type { TypeWithI18N } from '../../base/form/types'
|
||||
import type { Plugin } from '../../plugins/types'
|
||||
import type { CommonNodeType } from '../../workflow/types'
|
||||
import type { ActionKey } from '../constants'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { App } from '@/types/app'
|
||||
|
||||
|
|
@ -44,25 +42,4 @@ export type CommandSearchResult = {
|
|||
|
||||
export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult
|
||||
|
||||
// Legacy ActionItem for backward compatibility if needed, but we should move to ScopeDescriptor
|
||||
export type ActionItem = {
|
||||
key: ActionKey
|
||||
shortcut: string
|
||||
title: string | TypeWithI18N
|
||||
description: string
|
||||
/**
|
||||
* @deprecated use search() instead
|
||||
*/
|
||||
action?: (data: SearchResult) => void
|
||||
/**
|
||||
* @deprecated use search() instead
|
||||
*/
|
||||
searchFn?: (searchTerm: string) => SearchResult[]
|
||||
search: (
|
||||
query: string,
|
||||
searchTerm: string,
|
||||
locale?: string,
|
||||
) => (Promise<SearchResult[]> | SearchResult[])
|
||||
}
|
||||
|
||||
export type { ScopeContext, ScopeDescriptor } from './scope-registry'
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { ACTION_KEYS } from '../constants'
|
|||
import { scopeRegistry } from './scope-registry'
|
||||
|
||||
const scopeId = 'workflow-node'
|
||||
let scopeRegistered = false
|
||||
|
||||
const buildSearchHandler = (searchFn?: (searchTerm: string) => SearchResult[]): ScopeSearchHandler => {
|
||||
return async (_, searchTerm = '', _locale) => {
|
||||
|
|
@ -19,22 +20,22 @@ const buildSearchHandler = (searchFn?: (searchTerm: string) => SearchResult[]):
|
|||
}
|
||||
}
|
||||
|
||||
export const registerWorkflowNodeScope = () => {
|
||||
if (scopeRegistered)
|
||||
return
|
||||
|
||||
scopeRegistered = true
|
||||
scopeRegistry.register({
|
||||
id: scopeId,
|
||||
shortcut: ACTION_KEYS.NODE,
|
||||
title: 'Search Workflow Nodes',
|
||||
description: 'Find and jump to nodes in the current workflow by name or type',
|
||||
isAvailable: context => context.isWorkflowPage,
|
||||
search: buildSearchHandler(),
|
||||
})
|
||||
}
|
||||
|
||||
export const setWorkflowNodesSearchFn = (fn: (searchTerm: string) => SearchResult[]) => {
|
||||
registerWorkflowNodeScope()
|
||||
scopeRegistry.updateSearchHandler(scopeId, buildSearchHandler(fn))
|
||||
}
|
||||
|
||||
// Register the workflow nodes action
|
||||
scopeRegistry.register({
|
||||
id: scopeId,
|
||||
shortcut: ACTION_KEYS.NODE,
|
||||
title: 'Search Workflow Nodes',
|
||||
description: 'Find and jump to nodes in the current workflow by name or type',
|
||||
isAvailable: context => context.isWorkflowPage,
|
||||
search: buildSearchHandler(),
|
||||
})
|
||||
|
||||
// Legacy export if needed (though we should migrate away from it)
|
||||
export const workflowNodesAction = {
|
||||
key: ACTION_KEYS.NODE,
|
||||
search: async () => [], // Dummy implementation
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,9 +55,11 @@ const CommandSelector: FC<Props> = ({ scopes, onCommandSelect, searchFilter, com
|
|||
if (!searchFilter)
|
||||
return true
|
||||
|
||||
// Match against shortcut or title
|
||||
return scope.shortcut.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
|| scope.title.toLowerCase().includes(searchFilter.toLowerCase())
|
||||
// Match against shortcut/aliases or title
|
||||
const filterLower = searchFilter.toLowerCase()
|
||||
const shortcuts = [scope.shortcut, ...(scope.aliases || [])]
|
||||
return shortcuts.some(shortcut => shortcut.toLowerCase().includes(filterLower))
|
||||
|| scope.title.toLowerCase().includes(filterLower)
|
||||
}).map(scope => ({
|
||||
key: scope.shortcut, // Map to shortcut for UI display consistency
|
||||
shortcut: scope.shortcut,
|
||||
|
|
|
|||
|
|
@ -48,33 +48,25 @@ vi.mock('./context', () => ({
|
|||
GotoAnythingProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
||||
}))
|
||||
|
||||
const createScope = (id: ScopeDescriptor['id'], shortcut: string): ScopeDescriptor => ({
|
||||
id,
|
||||
shortcut,
|
||||
title: `${id} title`,
|
||||
description: `${id} desc`,
|
||||
search: vi.fn(),
|
||||
})
|
||||
|
||||
const scopesMock = [
|
||||
createScope('slash', '/'),
|
||||
createScope('app', '@app'),
|
||||
createScope('plugin', '@plugin'),
|
||||
]
|
||||
|
||||
type MatchAction = typeof import('./actions').matchAction
|
||||
type SearchAnything = typeof import('./actions').searchAnything
|
||||
|
||||
const useScopeRegistryMock = vi.fn(() => scopesMock)
|
||||
const matchActionMock = vi.fn<MatchAction>(() => undefined)
|
||||
const searchAnythingMock = vi.fn<SearchAnything>(async () => mockQueryResult.data)
|
||||
const registerDefaultScopesMock = vi.fn()
|
||||
const mockState = vi.hoisted(() => {
|
||||
const state = {
|
||||
scopes: [] as ScopeDescriptor[],
|
||||
useGotoAnythingScopesMock: vi.fn(() => state.scopes),
|
||||
matchActionMock: vi.fn<MatchAction>(() => undefined),
|
||||
searchAnythingMock: vi.fn<SearchAnything>(async () => []),
|
||||
}
|
||||
|
||||
return state
|
||||
})
|
||||
|
||||
vi.mock('./actions', () => ({
|
||||
__esModule: true,
|
||||
matchAction: (...args: Parameters<MatchAction>) => matchActionMock(...args),
|
||||
searchAnything: (...args: Parameters<SearchAnything>) => searchAnythingMock(...args),
|
||||
registerDefaultScopes: () => registerDefaultScopesMock(),
|
||||
matchAction: (...args: Parameters<MatchAction>) => mockState.matchActionMock(...args),
|
||||
searchAnything: (...args: Parameters<SearchAnything>) => mockState.searchAnythingMock(...args),
|
||||
useGotoAnythingScopes: () => mockState.useGotoAnythingScopesMock(),
|
||||
}))
|
||||
|
||||
vi.mock('./actions/commands', () => ({
|
||||
|
|
@ -90,9 +82,19 @@ vi.mock('./actions/commands/registry', () => ({
|
|||
},
|
||||
}))
|
||||
|
||||
vi.mock('./actions/scope-registry', () => ({
|
||||
useScopeRegistry: () => useScopeRegistryMock(),
|
||||
}))
|
||||
const createScope = (id: ScopeDescriptor['id'], shortcut: string): ScopeDescriptor => ({
|
||||
id,
|
||||
shortcut,
|
||||
title: `${id} title`,
|
||||
description: `${id} desc`,
|
||||
search: vi.fn(),
|
||||
})
|
||||
|
||||
const scopesMock = [
|
||||
createScope('slash', '/'),
|
||||
createScope('app', '@app'),
|
||||
createScope('plugin', '@plugin'),
|
||||
]
|
||||
|
||||
vi.mock('@/app/components/workflow/utils/common', () => ({
|
||||
getKeyboardKeyCodeBySystem: () => 'ctrl',
|
||||
|
|
@ -118,8 +120,10 @@ describe('GotoAnything', () => {
|
|||
routerPush.mockClear()
|
||||
Object.keys(keyPressHandlers).forEach(key => delete keyPressHandlers[key])
|
||||
mockQueryResult = { data: [], isLoading: false, isError: false, error: null }
|
||||
matchActionMock.mockReset()
|
||||
searchAnythingMock.mockClear()
|
||||
mockState.scopes = scopesMock
|
||||
mockState.matchActionMock.mockReset()
|
||||
mockState.searchAnythingMock.mockClear()
|
||||
mockState.searchAnythingMock.mockImplementation(async () => mockQueryResult.data)
|
||||
})
|
||||
|
||||
it('should open modal via shortcut and navigate to selected result', async () => {
|
||||
|
|
|
|||
|
|
@ -17,10 +17,9 @@ import { getKeyboardKeyCodeBySystem, isEventTargetInputArea, isMac } from '@/app
|
|||
import { selectWorkflowNode } from '@/app/components/workflow/utils/node-navigation'
|
||||
import { useGetLanguage } from '@/context/i18n'
|
||||
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
|
||||
import { matchAction, registerDefaultScopes, searchAnything } from './actions'
|
||||
import { matchAction, searchAnything, useGotoAnythingScopes } from './actions'
|
||||
import { executeCommand, SlashCommandProvider } from './actions/commands'
|
||||
import { slashCommandRegistry } from './actions/commands/registry'
|
||||
import { useScopeRegistry } from './actions/scope-registry'
|
||||
import CommandSelector from './command-selector'
|
||||
import { ACTION_KEYS, EMPTY_STATE_I18N_MAP, GROUP_HEADING_I18N_MAP } from './constants'
|
||||
import { GotoAnythingProvider, useGotoAnythingContext } from './context'
|
||||
|
|
@ -41,7 +40,7 @@ const GotoAnything: FC<Props> = ({
|
|||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
// Fetch scopes from registry based on context
|
||||
const scopes = useScopeRegistry({ isWorkflowPage, isRagPipelinePage }, registerDefaultScopes)
|
||||
const scopes = useGotoAnythingScopes({ isWorkflowPage, isRagPipelinePage })
|
||||
|
||||
const [activePlugin, setActivePlugin] = useState<Plugin>()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue