Feature/run cmd (#23822)
This commit is contained in:
26
web/app/components/goto-anything/actions/command-bus.ts
Normal file
26
web/app/components/goto-anything/actions/command-bus.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
export type CommandHandler = (args?: Record<string, any>) => void | Promise<void>
|
||||
|
||||
const handlers = new Map<string, CommandHandler>()
|
||||
|
||||
export const registerCommand = (name: string, handler: CommandHandler) => {
|
||||
handlers.set(name, handler)
|
||||
}
|
||||
|
||||
export const unregisterCommand = (name: string) => {
|
||||
handlers.delete(name)
|
||||
}
|
||||
|
||||
export const executeCommand = async (name: string, args?: Record<string, any>) => {
|
||||
const handler = handlers.get(name)
|
||||
if (!handler)
|
||||
return
|
||||
await handler(args)
|
||||
}
|
||||
|
||||
export const registerCommands = (map: Record<string, CommandHandler>) => {
|
||||
Object.entries(map).forEach(([name, handler]) => registerCommand(name, handler))
|
||||
}
|
||||
|
||||
export const unregisterCommands = (names: string[]) => {
|
||||
names.forEach(unregisterCommand)
|
||||
}
|
@@ -3,11 +3,13 @@ import { knowledgeAction } from './knowledge'
|
||||
import { pluginAction } from './plugin'
|
||||
import { workflowNodesAction } from './workflow-nodes'
|
||||
import type { ActionItem, SearchResult } from './types'
|
||||
import { commandAction } from './run'
|
||||
|
||||
export const Actions = {
|
||||
app: appAction,
|
||||
knowledge: knowledgeAction,
|
||||
plugin: pluginAction,
|
||||
run: commandAction,
|
||||
node: workflowNodesAction,
|
||||
}
|
||||
|
||||
|
33
web/app/components/goto-anything/actions/run-language.tsx
Normal file
33
web/app/components/goto-anything/actions/run-language.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { CommandSearchResult } from './types'
|
||||
import { languages } from '@/i18n-config/language'
|
||||
import { RiTranslate } from '@remixicon/react'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
|
||||
export const buildLanguageCommands = (query: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = languages.filter(item => item.supported && (
|
||||
!q || item.name.toLowerCase().includes(q) || String(item.value).toLowerCase().includes(q)
|
||||
))
|
||||
return list.map(item => ({
|
||||
id: `lang-${item.value}`,
|
||||
title: item.name,
|
||||
description: i18n.t('app.gotoAnything.actions.languageChangeDesc'),
|
||||
type: 'command' as const,
|
||||
data: { command: 'i18n.set', args: { locale: item.value } },
|
||||
}))
|
||||
}
|
||||
|
||||
export const buildLanguageRootItem = (): CommandSearchResult => {
|
||||
return {
|
||||
id: 'category-language',
|
||||
title: i18n.t('app.gotoAnything.actions.languageCategoryTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.languageCategoryDesc'),
|
||||
type: 'command',
|
||||
icon: (
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
|
||||
<RiTranslate className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
),
|
||||
data: { command: 'nav.search', args: { query: '@run language ' } },
|
||||
}
|
||||
}
|
61
web/app/components/goto-anything/actions/run-theme.tsx
Normal file
61
web/app/components/goto-anything/actions/run-theme.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
import type { CommandSearchResult } from './types'
|
||||
import type { ReactNode } from 'react'
|
||||
import { RiComputerLine, RiMoonLine, RiPaletteLine, RiSunLine } from '@remixicon/react'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
|
||||
const THEME_ITEMS: { id: 'light' | 'dark' | 'system'; titleKey: string; descKey: string; icon: ReactNode }[] = [
|
||||
{
|
||||
id: 'system',
|
||||
titleKey: 'app.gotoAnything.actions.themeSystem',
|
||||
descKey: 'app.gotoAnything.actions.themeSystemDesc',
|
||||
icon: <RiComputerLine className='h-4 w-4 text-text-tertiary' />,
|
||||
},
|
||||
{
|
||||
id: 'light',
|
||||
titleKey: 'app.gotoAnything.actions.themeLight',
|
||||
descKey: 'app.gotoAnything.actions.themeLightDesc',
|
||||
icon: <RiSunLine className='h-4 w-4 text-text-tertiary' />,
|
||||
},
|
||||
{
|
||||
id: 'dark',
|
||||
titleKey: 'app.gotoAnything.actions.themeDark',
|
||||
descKey: 'app.gotoAnything.actions.themeDarkDesc',
|
||||
icon: <RiMoonLine className='h-4 w-4 text-text-tertiary' />,
|
||||
},
|
||||
]
|
||||
|
||||
export const buildThemeCommands = (query: string, locale?: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const list = THEME_ITEMS.filter(item =>
|
||||
!q
|
||||
|| i18n.t(item.titleKey, { lng: locale }).toLowerCase().includes(q)
|
||||
|| item.id.includes(q),
|
||||
)
|
||||
return list.map(item => ({
|
||||
id: item.id,
|
||||
title: i18n.t(item.titleKey, { lng: locale }),
|
||||
description: i18n.t(item.descKey, { lng: locale }),
|
||||
type: 'command' as const,
|
||||
icon: (
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
|
||||
{item.icon}
|
||||
</div>
|
||||
),
|
||||
data: { command: 'theme.set', args: { value: item.id } },
|
||||
}))
|
||||
}
|
||||
|
||||
export const buildThemeRootItem = (): CommandSearchResult => {
|
||||
return {
|
||||
id: 'category-theme',
|
||||
title: i18n.t('app.gotoAnything.actions.themeCategoryTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.themeCategoryDesc'),
|
||||
type: 'command',
|
||||
icon: (
|
||||
<div className='flex h-6 w-6 items-center justify-center rounded-md border-[0.5px] border-divider-regular bg-components-panel-bg'>
|
||||
<RiPaletteLine className='h-4 w-4 text-text-tertiary' />
|
||||
</div>
|
||||
),
|
||||
data: { command: 'nav.search', args: { query: '@run theme ' } },
|
||||
}
|
||||
}
|
97
web/app/components/goto-anything/actions/run.tsx
Normal file
97
web/app/components/goto-anything/actions/run.tsx
Normal file
@@ -0,0 +1,97 @@
|
||||
'use client'
|
||||
import { useEffect } from 'react'
|
||||
import type { ActionItem, CommandSearchResult } from './types'
|
||||
import { buildLanguageCommands, buildLanguageRootItem } from './run-language'
|
||||
import { buildThemeCommands, buildThemeRootItem } from './run-theme'
|
||||
import i18n from '@/i18n-config/i18next-config'
|
||||
import { executeCommand, registerCommands, unregisterCommands } from './command-bus'
|
||||
import { useTheme } from 'next-themes'
|
||||
import { setLocaleOnClient } from '@/i18n-config'
|
||||
|
||||
const rootParser = (query: string): CommandSearchResult[] => {
|
||||
const q = query.toLowerCase()
|
||||
const items: CommandSearchResult[] = []
|
||||
if (!q || 'theme'.includes(q))
|
||||
items.push(buildThemeRootItem())
|
||||
if (!q || 'language'.includes(q) || 'lang'.includes(q))
|
||||
items.push(buildLanguageRootItem())
|
||||
return items
|
||||
}
|
||||
|
||||
type RunContext = {
|
||||
setTheme?: (value: 'light' | 'dark' | 'system') => void
|
||||
setLocale?: (locale: string) => Promise<void>
|
||||
search?: (query: string) => void
|
||||
}
|
||||
|
||||
export const commandAction: ActionItem = {
|
||||
key: '@run',
|
||||
shortcut: '@run',
|
||||
title: i18n.t('app.gotoAnything.actions.runTitle'),
|
||||
description: i18n.t('app.gotoAnything.actions.runDesc'),
|
||||
action: (result) => {
|
||||
if (result.type !== 'command') return
|
||||
const { command, args } = result.data
|
||||
if (command === 'theme.set') {
|
||||
executeCommand('theme.set', args)
|
||||
return
|
||||
}
|
||||
if (command === 'i18n.set') {
|
||||
executeCommand('i18n.set', args)
|
||||
return
|
||||
}
|
||||
if (command === 'nav.search')
|
||||
executeCommand('nav.search', args)
|
||||
},
|
||||
search: async (_, searchTerm = '') => {
|
||||
const q = searchTerm.trim()
|
||||
if (q.startsWith('theme'))
|
||||
return buildThemeCommands(q.replace(/^theme\s*/, ''), i18n.language)
|
||||
if (q.startsWith('language') || q.startsWith('lang'))
|
||||
return buildLanguageCommands(q.replace(/^(language|lang)\s*/, ''))
|
||||
|
||||
// root categories
|
||||
return rootParser(q)
|
||||
},
|
||||
}
|
||||
|
||||
// Register/unregister default handlers for @run commands with external dependencies.
|
||||
export const registerRunCommands = (deps: {
|
||||
setTheme?: (value: 'light' | 'dark' | 'system') => void
|
||||
setLocale?: (locale: string) => Promise<void>
|
||||
search?: (query: string) => void
|
||||
}) => {
|
||||
registerCommands({
|
||||
'theme.set': async (args) => {
|
||||
deps.setTheme?.(args?.value)
|
||||
},
|
||||
'i18n.set': async (args) => {
|
||||
const locale = args?.locale
|
||||
if (locale)
|
||||
await deps.setLocale?.(locale)
|
||||
},
|
||||
'nav.search': (args) => {
|
||||
const q = args?.query
|
||||
if (q)
|
||||
deps.search?.(q)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const unregisterRunCommands = () => {
|
||||
unregisterCommands(['theme.set', 'i18n.set', 'nav.search'])
|
||||
}
|
||||
|
||||
export const RunCommandProvider = ({ onNavSearch }: { onNavSearch?: (q: string) => void }) => {
|
||||
const theme = useTheme()
|
||||
useEffect(() => {
|
||||
registerRunCommands({
|
||||
setTheme: theme.setTheme,
|
||||
setLocale: setLocaleOnClient,
|
||||
search: onNavSearch,
|
||||
})
|
||||
return () => unregisterRunCommands()
|
||||
}, [theme.setTheme, onNavSearch])
|
||||
|
||||
return null
|
||||
}
|
@@ -5,7 +5,7 @@ import type { Plugin } from '../../plugins/types'
|
||||
import type { DataSet } from '@/models/datasets'
|
||||
import type { CommonNodeType } from '../../workflow/types'
|
||||
|
||||
export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node'
|
||||
export type SearchResultType = 'app' | 'knowledge' | 'plugin' | 'workflow-node' | 'command'
|
||||
|
||||
export type BaseSearchResult<T = any> = {
|
||||
id: string
|
||||
@@ -37,10 +37,14 @@ export type WorkflowNodeSearchResult = {
|
||||
}
|
||||
} & BaseSearchResult<CommonNodeType>
|
||||
|
||||
export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult
|
||||
export type CommandSearchResult = {
|
||||
type: 'command'
|
||||
} & BaseSearchResult<{ command: string; args?: Record<string, any> }>
|
||||
|
||||
export type SearchResult = AppSearchResult | PluginSearchResult | KnowledgeSearchResult | WorkflowNodeSearchResult | CommandSearchResult
|
||||
|
||||
export type ActionItem = {
|
||||
key: '@app' | '@knowledge' | '@plugin' | '@node'
|
||||
key: '@app' | '@knowledge' | '@plugin' | '@node' | '@run'
|
||||
shortcut: string
|
||||
title: string | TypeWithI18N
|
||||
description: string
|
||||
|
@@ -73,6 +73,7 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co
|
||||
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
|
||||
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
|
||||
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
|
||||
'@run': 'app.gotoAnything.actions.runDesc',
|
||||
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
|
||||
}
|
||||
return t(keyMap[action.key])
|
||||
|
@@ -18,6 +18,7 @@ import InstallFromMarketplace from '../plugins/install-plugin/install-from-marke
|
||||
import type { Plugin } from '../plugins/types'
|
||||
import { Command } from 'cmdk'
|
||||
import CommandSelector from './command-selector'
|
||||
import { RunCommandProvider } from './actions/run'
|
||||
|
||||
type Props = {
|
||||
onHide?: () => void
|
||||
@@ -33,7 +34,11 @@ const GotoAnything: FC<Props> = ({
|
||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||
const [cmdVal, setCmdVal] = useState<string>('')
|
||||
const inputRef = useRef<HTMLInputElement>(null)
|
||||
|
||||
const handleNavSearch = useCallback((q: string) => {
|
||||
setShow(true)
|
||||
setSearchQuery(q)
|
||||
requestAnimationFrame(() => inputRef.current?.focus())
|
||||
}, [])
|
||||
// Filter actions based on context
|
||||
const Actions = useMemo(() => {
|
||||
// Create a filtered copy of actions based on current page context
|
||||
@@ -43,8 +48,8 @@ const GotoAnything: FC<Props> = ({
|
||||
}
|
||||
else {
|
||||
// Exclude node action on non-workflow pages
|
||||
const { app, knowledge, plugin } = AllActions
|
||||
return { app, knowledge, plugin }
|
||||
const { app, knowledge, plugin, run } = AllActions
|
||||
return { app, knowledge, plugin, run }
|
||||
}
|
||||
}, [isWorkflowPage])
|
||||
|
||||
@@ -128,6 +133,11 @@ const GotoAnything: FC<Props> = ({
|
||||
setSearchQuery('')
|
||||
|
||||
switch (result.type) {
|
||||
case 'command': {
|
||||
const action = Object.values(Actions).find(a => a.key === '@run')
|
||||
action?.action?.(result)
|
||||
break
|
||||
}
|
||||
case 'plugin':
|
||||
setActivePlugin(result.data)
|
||||
break
|
||||
@@ -381,6 +391,7 @@ const GotoAnything: FC<Props> = ({
|
||||
</div>
|
||||
|
||||
</Modal>
|
||||
<RunCommandProvider onNavSearch={handleNavSearch} />
|
||||
{
|
||||
activePlugin && (
|
||||
<InstallFromMarketplace
|
||||
|
@@ -279,6 +279,19 @@ const translation = {
|
||||
searchWorkflowNodes: 'Search Workflow Nodes',
|
||||
searchWorkflowNodesDesc: 'Find and jump to nodes in the current workflow by name or type',
|
||||
searchWorkflowNodesHelp: 'This feature only works when viewing a workflow. Navigate to a workflow first.',
|
||||
runTitle: 'Commands',
|
||||
runDesc: 'Run quick commands (theme, language, ...)',
|
||||
themeCategoryTitle: 'Theme',
|
||||
themeCategoryDesc: 'Switch application theme',
|
||||
themeSystem: 'System Theme',
|
||||
themeSystemDesc: 'Follow your OS appearance',
|
||||
themeLight: 'Light Theme',
|
||||
themeLightDesc: 'Use light appearance',
|
||||
themeDark: 'Dark Theme',
|
||||
themeDarkDesc: 'Use dark appearance',
|
||||
languageCategoryTitle: 'Language',
|
||||
languageCategoryDesc: 'Switch interface language',
|
||||
languageChangeDesc: 'Change UI language',
|
||||
},
|
||||
emptyState: {
|
||||
noAppsFound: 'No apps found',
|
||||
|
@@ -278,6 +278,19 @@ const translation = {
|
||||
searchWorkflowNodes: '搜索工作流节点',
|
||||
searchWorkflowNodesDesc: '按名称或类型查找并跳转到当前工作流中的节点',
|
||||
searchWorkflowNodesHelp: '此功能仅在查看工作流时有效。首先导航到工作流。',
|
||||
runTitle: '命令',
|
||||
runDesc: '快速执行命令(主题、语言等)',
|
||||
themeCategoryTitle: '主题',
|
||||
themeCategoryDesc: '切换应用主题',
|
||||
themeSystem: '系统主题',
|
||||
themeSystemDesc: '跟随系统外观',
|
||||
themeLight: '浅色主题',
|
||||
themeLightDesc: '使用浅色外观',
|
||||
themeDark: '深色主题',
|
||||
themeDarkDesc: '使用深色外观',
|
||||
languageCategoryTitle: '语言',
|
||||
languageCategoryDesc: '切换界面语言',
|
||||
languageChangeDesc: '更改界面语言',
|
||||
},
|
||||
emptyState: {
|
||||
noAppsFound: '未找到应用',
|
||||
|
Reference in New Issue
Block a user