From 75b7d269e1a03bbfc0042e607386838e13858d1c Mon Sep 17 00:00:00 2001 From: yyh Date: Sat, 27 Dec 2025 14:51:45 +0800 Subject: [PATCH] refactor: centralize action keys and internationalization mappings - Introduced a new `constants.ts` file to centralize action keys and i18n mappings for slash commands and scope actions. - Updated `command-selector` and various action files to utilize the new constants for improved maintainability and readability. - Removed hardcoded strings in favor of the new mappings, ensuring consistency across the application. --- .../components/goto-anything/actions/app.tsx | 5 +- .../actions/commands/language.tsx | 3 +- .../actions/commands/registry.ts | 29 ++++++------ .../goto-anything/actions/commands/slash.tsx | 8 ++-- .../goto-anything/actions/commands/types.ts | 31 ++++++++++++- .../components/goto-anything/actions/index.ts | 5 +- .../goto-anything/actions/knowledge.tsx | 3 +- .../goto-anything/actions/plugin.tsx | 5 +- .../actions/rag-pipeline-nodes.tsx | 5 +- .../components/goto-anything/actions/types.ts | 7 +-- .../goto-anything/actions/workflow-nodes.tsx | 5 +- .../goto-anything/command-selector.tsx | 31 ++----------- web/app/components/goto-anything/constants.ts | 46 +++++++++++++++++++ web/app/components/goto-anything/index.tsx | 3 +- 14 files changed, 125 insertions(+), 61 deletions(-) create mode 100644 web/app/components/goto-anything/constants.ts diff --git a/web/app/components/goto-anything/actions/app.tsx b/web/app/components/goto-anything/actions/app.tsx index 496475eacb..d391556604 100644 --- a/web/app/components/goto-anything/actions/app.tsx +++ b/web/app/components/goto-anything/actions/app.tsx @@ -4,6 +4,7 @@ import { fetchAppList } from '@/service/apps' import { getRedirectionPath } from '@/utils/app-redirection' import { AppTypeIcon } from '../../app/type-selector' import AppIcon from '../../base/app-icon' +import { ACTION_KEYS } from '../constants' const parser = (apps: App[]): AppSearchResult[] => { return apps.map(app => ({ @@ -36,8 +37,8 @@ const parser = (apps: App[]): AppSearchResult[] => { } export const appAction: ActionItem = { - key: '@app', - shortcut: '@app', + key: ACTION_KEYS.APP, + shortcut: ACTION_KEYS.APP, title: 'Search Applications', description: 'Search and navigate to your applications', // action, diff --git a/web/app/components/goto-anything/actions/commands/language.tsx b/web/app/components/goto-anything/actions/commands/language.tsx index f5644a0518..d66a61f722 100644 --- a/web/app/components/goto-anything/actions/commands/language.tsx +++ b/web/app/components/goto-anything/actions/commands/language.tsx @@ -1,12 +1,13 @@ import type { CommandSearchResult } from '../types' import type { SlashCommandHandler } from './types' +import type { Locale } from '@/i18n-config/language' import i18n from '@/i18n-config/i18next-config' import { languages } from '@/i18n-config/language' import { registerCommands, unregisterCommands } from './command-bus' // Language dependency types type LanguageDeps = { - setLocale?: (locale: string) => Promise + setLocale?: (locale: Locale, reloadPage?: boolean) => Promise } const buildLanguageCommands = (query: string): CommandSearchResult[] => { diff --git a/web/app/components/goto-anything/actions/commands/registry.ts b/web/app/components/goto-anything/actions/commands/registry.ts index 51beef4c0b..94321a1916 100644 --- a/web/app/components/goto-anything/actions/commands/registry.ts +++ b/web/app/components/goto-anything/actions/commands/registry.ts @@ -6,20 +6,21 @@ import type { SlashCommandHandler } from './types' * Responsible for managing registration, lookup, and search of all slash commands */ export class SlashCommandRegistry { - private commands = new Map() - private commandDeps = new Map() + private commands = new Map>() + private commandDeps = new Map() /** * Register command handler */ - register(handler: SlashCommandHandler, deps?: TDeps) { + register(handler: SlashCommandHandler, deps?: TDeps) { // Register main command name - this.commands.set(handler.name, handler) + // Cast to unknown first, then to SlashCommandHandler to handle generic type variance + this.commands.set(handler.name, handler as SlashCommandHandler) // Register aliases if (handler.aliases) { handler.aliases.forEach((alias) => { - this.commands.set(alias, handler) + this.commands.set(alias, handler as SlashCommandHandler) }) } @@ -57,7 +58,7 @@ export class SlashCommandRegistry { /** * Find command handler */ - findCommand(commandName: string): SlashCommandHandler | undefined { + findCommand(commandName: string): SlashCommandHandler | undefined { return this.commands.get(commandName) } @@ -65,7 +66,7 @@ export class SlashCommandRegistry { * Smart partial command matching * Prioritize alias matching, then match command name prefix */ - private findBestPartialMatch(partialName: string): SlashCommandHandler | undefined { + private findBestPartialMatch(partialName: string): SlashCommandHandler | undefined { const lowerPartial = partialName.toLowerCase() // First check if any alias starts with this @@ -81,7 +82,7 @@ export class SlashCommandRegistry { /** * Find handler by alias prefix */ - private findHandlerByAliasPrefix(prefix: string): SlashCommandHandler | undefined { + private findHandlerByAliasPrefix(prefix: string): SlashCommandHandler | undefined { for (const handler of this.getAllCommands()) { if (handler.aliases?.some(alias => alias.toLowerCase().startsWith(prefix))) return handler @@ -92,7 +93,7 @@ export class SlashCommandRegistry { /** * Find handler by name prefix */ - private findHandlerByNamePrefix(prefix: string): SlashCommandHandler | undefined { + private findHandlerByNamePrefix(prefix: string): SlashCommandHandler | undefined { return this.getAllCommands().find(handler => handler.name.toLowerCase().startsWith(prefix), ) @@ -101,8 +102,8 @@ export class SlashCommandRegistry { /** * Get all registered commands (deduplicated) */ - getAllCommands(): SlashCommandHandler[] { - const uniqueCommands = new Map() + getAllCommands(): SlashCommandHandler[] { + const uniqueCommands = new Map>() this.commands.forEach((handler) => { uniqueCommands.set(handler.name, handler) }) @@ -113,7 +114,7 @@ export class SlashCommandRegistry { * Get all available commands in current context (deduplicated and filtered) * Commands without isAvailable method are considered always available */ - getAvailableCommands(): SlashCommandHandler[] { + getAvailableCommands(): SlashCommandHandler[] { return this.getAllCommands().filter(handler => this.isCommandAvailable(handler)) } @@ -228,7 +229,7 @@ export class SlashCommandRegistry { /** * Get command dependencies */ - getCommandDependencies(commandName: string): any { + getCommandDependencies(commandName: string): unknown { return this.commandDeps.get(commandName) } @@ -236,7 +237,7 @@ export class SlashCommandRegistry { * Determine if a command is available in the current context. * Defaults to true when a handler does not implement the guard. */ - private isCommandAvailable(handler: SlashCommandHandler) { + private isCommandAvailable(handler: SlashCommandHandler) { return handler.isAvailable?.() ?? true } } diff --git a/web/app/components/goto-anything/actions/commands/slash.tsx b/web/app/components/goto-anything/actions/commands/slash.tsx index 1cab3a358c..2eaca0beae 100644 --- a/web/app/components/goto-anything/actions/commands/slash.tsx +++ b/web/app/components/goto-anything/actions/commands/slash.tsx @@ -1,9 +1,11 @@ 'use client' import type { ActionItem } from '../types' +import type { SlashCommandDependencies } from './types' import { useTheme } from 'next-themes' import { useEffect } from 'react' import { setLocaleOnClient } from '@/i18n-config' 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' @@ -16,8 +18,8 @@ import { themeCommand } from './theme' import { zenCommand } from './zen' export const slashAction: ActionItem = { - key: '/', - shortcut: '/', + key: ACTION_KEYS.SLASH, + shortcut: ACTION_KEYS.SLASH, title: i18n.t('app.gotoAnything.actions.slashTitle'), description: i18n.t('app.gotoAnything.actions.slashDesc'), action: (result) => { @@ -33,7 +35,7 @@ export const slashAction: ActionItem = { } // Register/unregister default handlers for slash commands with external dependencies. -export const registerSlashCommands = (deps: Record) => { +export const registerSlashCommands = (deps: SlashCommandDependencies) => { // Register command handlers to the registry system with their respective dependencies slashCommandRegistry.register(themeCommand, { setTheme: deps.setTheme }) slashCommandRegistry.register(languageCommand, { setLocale: deps.setLocale }) diff --git a/web/app/components/goto-anything/actions/commands/types.ts b/web/app/components/goto-anything/actions/commands/types.ts index 528883c25f..ccf8cdd881 100644 --- a/web/app/components/goto-anything/actions/commands/types.ts +++ b/web/app/components/goto-anything/actions/commands/types.ts @@ -1,10 +1,11 @@ import type { CommandSearchResult } from '../types' +import type { Locale } from '@/i18n-config/language' /** * Slash command handler interface * Each slash command should implement this interface */ -export type SlashCommandHandler = { +export type SlashCommandHandler = { /** Command name (e.g., 'theme', 'language') */ name: string @@ -51,3 +52,31 @@ export type SlashCommandHandler = { */ unregister?: () => void } + +/** + * Theme command dependencies + */ +export type ThemeCommandDeps = { + setTheme?: (value: 'light' | 'dark' | 'system') => void +} + +/** + * Language command dependencies + */ +export type LanguageCommandDeps = { + setLocale?: (locale: Locale, reloadPage?: boolean) => Promise +} + +/** + * Commands without external dependencies + */ +export type NoDepsCommandDeps = Record + +/** + * Union type of all slash command dependencies + * Used for type-safe dependency injection in registerSlashCommands + */ +export type SlashCommandDependencies = { + setTheme?: (value: 'light' | 'dark' | 'system') => void + setLocale?: (locale: Locale, reloadPage?: boolean) => Promise +} diff --git a/web/app/components/goto-anything/actions/index.ts b/web/app/components/goto-anything/actions/index.ts index 024b6bfd2c..7c091b92ef 100644 --- a/web/app/components/goto-anything/actions/index.ts +++ b/web/app/components/goto-anything/actions/index.ts @@ -164,6 +164,7 @@ */ import type { ActionItem, SearchResult } from './types' +import { ACTION_KEYS } from '../constants' import { appAction } from './app' import { slashAction } from './commands' import { slashCommandRegistry } from './commands/registry' @@ -234,7 +235,7 @@ export const searchAnything = async ( const globalSearchActions = Object.values(dynamicActions || Actions) // Exclude slash commands from general search results - .filter(action => action.key !== '/') + .filter(action => action.key !== ACTION_KEYS.SLASH) // Use Promise.allSettled to handle partial failures gracefully const searchPromises = globalSearchActions.map(async (action) => { @@ -272,7 +273,7 @@ export const searchAnything = async ( export const matchAction = (query: string, actions: Record) => { return Object.values(actions).find((action) => { // Special handling for slash commands - if (action.key === '/') { + if (action.key === ACTION_KEYS.SLASH) { // Get all registered commands from the registry const allCommands = slashCommandRegistry.getAllCommands() diff --git a/web/app/components/goto-anything/actions/knowledge.tsx b/web/app/components/goto-anything/actions/knowledge.tsx index 9531a3551f..c338386446 100644 --- a/web/app/components/goto-anything/actions/knowledge.tsx +++ b/web/app/components/goto-anything/actions/knowledge.tsx @@ -3,6 +3,7 @@ import type { DataSet } from '@/models/datasets' import { fetchDatasets } from '@/service/datasets' import { cn } from '@/utils/classnames' import { Folder } from '../../base/icons/src/vender/solid/files' +import { ACTION_KEYS } from '../constants' const EXTERNAL_PROVIDER = 'external' as const const isExternalProvider = (provider: string): boolean => provider === EXTERNAL_PROVIDER @@ -31,7 +32,7 @@ const parser = (datasets: DataSet[]): KnowledgeSearchResult[] => { } export const knowledgeAction: ActionItem = { - key: '@knowledge', + key: ACTION_KEYS.KNOWLEDGE, shortcut: '@kb', title: 'Search Knowledge Bases', description: 'Search and navigate to your knowledge bases', diff --git a/web/app/components/goto-anything/actions/plugin.tsx b/web/app/components/goto-anything/actions/plugin.tsx index 07197b8198..7c3baa2381 100644 --- a/web/app/components/goto-anything/actions/plugin.tsx +++ b/web/app/components/goto-anything/actions/plugin.tsx @@ -4,6 +4,7 @@ import { renderI18nObject } from '@/i18n-config' import { postMarketplace } from '@/service/base' import Icon from '../../plugins/card/base/card-icon' import { getPluginIconInMarketplace } from '../../plugins/marketplace/utils' +import { ACTION_KEYS } from '../constants' const parser = (plugins: Plugin[], locale: string): PluginSearchResult[] => { return plugins.map((plugin) => { @@ -19,8 +20,8 @@ const parser = (plugins: Plugin[], locale: string): PluginSearchResult[] => { } export const pluginAction: ActionItem = { - key: '@plugin', - shortcut: '@plugin', + key: ACTION_KEYS.PLUGIN, + shortcut: ACTION_KEYS.PLUGIN, title: 'Search Plugins', description: 'Search and navigate to your plugins', search: async (_, searchTerm = '', locale) => { diff --git a/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx b/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx index dc632e4999..430b7087cb 100644 --- a/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx +++ b/web/app/components/goto-anything/actions/rag-pipeline-nodes.tsx @@ -1,9 +1,10 @@ import type { ActionItem } from './types' +import { ACTION_KEYS } from '../constants' // Create the RAG pipeline nodes action export const ragPipelineNodesAction: ActionItem = { - key: '@node', - shortcut: '@node', + key: ACTION_KEYS.NODE, + shortcut: ACTION_KEYS.NODE, title: 'Search RAG Pipeline Nodes', description: 'Find and jump to nodes in the current RAG pipeline by name or type', searchFn: undefined, // Will be set by useRagPipelineSearch hook diff --git a/web/app/components/goto-anything/actions/types.ts b/web/app/components/goto-anything/actions/types.ts index 838195ad85..1963a808fb 100644 --- a/web/app/components/goto-anything/actions/types.ts +++ b/web/app/components/goto-anything/actions/types.ts @@ -2,12 +2,13 @@ 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' export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command' -export type BaseSearchResult = { +export type BaseSearchResult = { id: string title: string description?: string @@ -39,12 +40,12 @@ export type WorkflowNodeSearchResult = { export type CommandSearchResult = { type: 'command' -} & BaseSearchResult<{ command: string, args?: Record }> +} & BaseSearchResult<{ command: string, args?: Record }> export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult export type ActionItem = { - key: '@app' | '@knowledge' | '@plugin' | '@node' | '/' + key: ActionKey shortcut: string title: string | TypeWithI18N description: string diff --git a/web/app/components/goto-anything/actions/workflow-nodes.tsx b/web/app/components/goto-anything/actions/workflow-nodes.tsx index b9aa61705b..9b743f0108 100644 --- a/web/app/components/goto-anything/actions/workflow-nodes.tsx +++ b/web/app/components/goto-anything/actions/workflow-nodes.tsx @@ -1,9 +1,10 @@ import type { ActionItem } from './types' +import { ACTION_KEYS } from '../constants' // Create the workflow nodes action export const workflowNodesAction: ActionItem = { - key: '@node', - shortcut: '@node', + key: ACTION_KEYS.NODE, + shortcut: ACTION_KEYS.NODE, title: 'Search Workflow Nodes', description: 'Find and jump to nodes in the current workflow by name or type', searchFn: undefined, // Will be set by useWorkflowSearch hook diff --git a/web/app/components/goto-anything/command-selector.tsx b/web/app/components/goto-anything/command-selector.tsx index bef6bab347..fbc2339b28 100644 --- a/web/app/components/goto-anything/command-selector.tsx +++ b/web/app/components/goto-anything/command-selector.tsx @@ -5,6 +5,7 @@ import { usePathname } from 'next/navigation' import { useEffect, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { slashCommandRegistry } from './actions/commands/registry' +import { ACTION_KEYS, SCOPE_ACTION_I18N_MAP, SLASH_COMMAND_I18N_MAP } from './constants' type Props = { actions: Record @@ -49,7 +50,7 @@ const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, co return Object.values(actions).filter((action) => { // Exclude slash action when in @ mode - if (action.key === '/') + if (action.key === ACTION_KEYS.SLASH) return false if (!searchFilter) return true @@ -106,32 +107,8 @@ const CommandSelector: FC = ({ actions, onCommandSelect, searchFilter, co {isSlashMode - ? ( - (() => { - const slashKeyMap: Record = { - '/theme': 'app.gotoAnything.actions.themeCategoryDesc', - '/language': 'app.gotoAnything.actions.languageChangeDesc', - '/account': 'app.gotoAnything.actions.accountDesc', - '/feedback': 'app.gotoAnything.actions.feedbackDesc', - '/docs': 'app.gotoAnything.actions.docDesc', - '/community': 'app.gotoAnything.actions.communityDesc', - '/zen': 'app.gotoAnything.actions.zenDesc', - '/banana': 'app.gotoAnything.actions.vibeDesc', - } - return t((slashKeyMap[item.key] || item.description) as any) - })() - ) - : ( - (() => { - const keyMap: Record = { - '@app': 'app.gotoAnything.actions.searchApplicationsDesc', - '@plugin': 'app.gotoAnything.actions.searchPluginsDesc', - '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', - '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', - } - return t(keyMap[item.key] as any) as string - })() - )} + ? t((SLASH_COMMAND_I18N_MAP[item.key] || item.description) as any) + : t((SCOPE_ACTION_I18N_MAP[item.key] || item.description) as any)} ))} diff --git a/web/app/components/goto-anything/constants.ts b/web/app/components/goto-anything/constants.ts new file mode 100644 index 0000000000..39fbc601fe --- /dev/null +++ b/web/app/components/goto-anything/constants.ts @@ -0,0 +1,46 @@ +/** + * Goto Anything Constants + * Centralized constants for action keys, command mappings, and i18n keys + */ + +/** + * Action keys for scope-based searches + */ +export const ACTION_KEYS = { + APP: '@app', + KNOWLEDGE: '@knowledge', + PLUGIN: '@plugin', + NODE: '@node', + SLASH: '/', +} as const + +/** + * Type-safe action key union type + */ +export type ActionKey = typeof ACTION_KEYS[keyof typeof ACTION_KEYS] + +/** + * Slash command i18n key mappings + * Maps slash command keys to their corresponding i18n translation keys + */ +export const SLASH_COMMAND_I18N_MAP: Record = { + '/theme': 'app.gotoAnything.actions.themeCategoryDesc', + '/language': 'app.gotoAnything.actions.languageChangeDesc', + '/account': 'app.gotoAnything.actions.accountDesc', + '/feedback': 'app.gotoAnything.actions.feedbackDesc', + '/docs': 'app.gotoAnything.actions.docDesc', + '/community': 'app.gotoAnything.actions.communityDesc', + '/zen': 'app.gotoAnything.actions.zenDesc', + '/banana': 'app.gotoAnything.actions.vibeDesc', +} as const + +/** + * Scope action i18n key mappings + * Maps scope action keys to their corresponding i18n translation keys + */ +export const SCOPE_ACTION_I18N_MAP: Record = { + '@app': 'app.gotoAnything.actions.searchApplicationsDesc', + '@plugin': 'app.gotoAnything.actions.searchPluginsDesc', + '@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc', + '@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc', +} as const diff --git a/web/app/components/goto-anything/index.tsx b/web/app/components/goto-anything/index.tsx index a87340f3d2..8552ba4585 100644 --- a/web/app/components/goto-anything/index.tsx +++ b/web/app/components/goto-anything/index.tsx @@ -21,6 +21,7 @@ import { createActions, matchAction, searchAnything } from './actions' import { SlashCommandProvider } from './actions/commands' import { slashCommandRegistry } from './actions/commands/registry' import CommandSelector from './command-selector' +import { ACTION_KEYS } from './constants' import { GotoAnythingProvider, useGotoAnythingContext } from './context' type Props = { @@ -98,7 +99,7 @@ const GotoAnything: FC = ({ if (!action) return 'general' - return action.key === '/' ? '@command' : action.key + return action.key === ACTION_KEYS.SLASH ? '@command' : action.key }, [searchQueryDebouncedValue, Actions, isCommandsMode, searchQuery]) const { data: searchResults = [], isLoading, isError, error } = useQuery(