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 { pluginAction } from './plugin'
|
||||||
import { workflowNodesAction } from './workflow-nodes'
|
import { workflowNodesAction } from './workflow-nodes'
|
||||||
import type { ActionItem, SearchResult } from './types'
|
import type { ActionItem, SearchResult } from './types'
|
||||||
|
import { commandAction } from './run'
|
||||||
|
|
||||||
export const Actions = {
|
export const Actions = {
|
||||||
app: appAction,
|
app: appAction,
|
||||||
knowledge: knowledgeAction,
|
knowledge: knowledgeAction,
|
||||||
plugin: pluginAction,
|
plugin: pluginAction,
|
||||||
|
run: commandAction,
|
||||||
node: workflowNodesAction,
|
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 { DataSet } from '@/models/datasets'
|
||||||
import type { CommonNodeType } from '../../workflow/types'
|
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> = {
|
export type BaseSearchResult<T = any> = {
|
||||||
id: string
|
id: string
|
||||||
@@ -37,10 +37,14 @@ export type WorkflowNodeSearchResult = {
|
|||||||
}
|
}
|
||||||
} & BaseSearchResult<CommonNodeType>
|
} & 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 = {
|
export type ActionItem = {
|
||||||
key: '@app' | '@knowledge' | '@plugin' | '@node'
|
key: '@app' | '@knowledge' | '@plugin' | '@node' | '@run'
|
||||||
shortcut: string
|
shortcut: string
|
||||||
title: string | TypeWithI18N
|
title: string | TypeWithI18N
|
||||||
description: string
|
description: string
|
||||||
|
@@ -73,6 +73,7 @@ const CommandSelector: FC<Props> = ({ actions, onCommandSelect, searchFilter, co
|
|||||||
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
|
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
|
||||||
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
|
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
|
||||||
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
|
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
|
||||||
|
'@run': 'app.gotoAnything.actions.runDesc',
|
||||||
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
|
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
|
||||||
}
|
}
|
||||||
return t(keyMap[action.key])
|
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 type { Plugin } from '../plugins/types'
|
||||||
import { Command } from 'cmdk'
|
import { Command } from 'cmdk'
|
||||||
import CommandSelector from './command-selector'
|
import CommandSelector from './command-selector'
|
||||||
|
import { RunCommandProvider } from './actions/run'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onHide?: () => void
|
onHide?: () => void
|
||||||
@@ -33,7 +34,11 @@ const GotoAnything: FC<Props> = ({
|
|||||||
const [searchQuery, setSearchQuery] = useState<string>('')
|
const [searchQuery, setSearchQuery] = useState<string>('')
|
||||||
const [cmdVal, setCmdVal] = useState<string>('')
|
const [cmdVal, setCmdVal] = useState<string>('')
|
||||||
const inputRef = useRef<HTMLInputElement>(null)
|
const inputRef = useRef<HTMLInputElement>(null)
|
||||||
|
const handleNavSearch = useCallback((q: string) => {
|
||||||
|
setShow(true)
|
||||||
|
setSearchQuery(q)
|
||||||
|
requestAnimationFrame(() => inputRef.current?.focus())
|
||||||
|
}, [])
|
||||||
// Filter actions based on context
|
// Filter actions based on context
|
||||||
const Actions = useMemo(() => {
|
const Actions = useMemo(() => {
|
||||||
// Create a filtered copy of actions based on current page context
|
// Create a filtered copy of actions based on current page context
|
||||||
@@ -43,8 +48,8 @@ const GotoAnything: FC<Props> = ({
|
|||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Exclude node action on non-workflow pages
|
// Exclude node action on non-workflow pages
|
||||||
const { app, knowledge, plugin } = AllActions
|
const { app, knowledge, plugin, run } = AllActions
|
||||||
return { app, knowledge, plugin }
|
return { app, knowledge, plugin, run }
|
||||||
}
|
}
|
||||||
}, [isWorkflowPage])
|
}, [isWorkflowPage])
|
||||||
|
|
||||||
@@ -128,6 +133,11 @@ const GotoAnything: FC<Props> = ({
|
|||||||
setSearchQuery('')
|
setSearchQuery('')
|
||||||
|
|
||||||
switch (result.type) {
|
switch (result.type) {
|
||||||
|
case 'command': {
|
||||||
|
const action = Object.values(Actions).find(a => a.key === '@run')
|
||||||
|
action?.action?.(result)
|
||||||
|
break
|
||||||
|
}
|
||||||
case 'plugin':
|
case 'plugin':
|
||||||
setActivePlugin(result.data)
|
setActivePlugin(result.data)
|
||||||
break
|
break
|
||||||
@@ -381,6 +391,7 @@ const GotoAnything: FC<Props> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
</Modal>
|
</Modal>
|
||||||
|
<RunCommandProvider onNavSearch={handleNavSearch} />
|
||||||
{
|
{
|
||||||
activePlugin && (
|
activePlugin && (
|
||||||
<InstallFromMarketplace
|
<InstallFromMarketplace
|
||||||
|
@@ -279,6 +279,19 @@ const translation = {
|
|||||||
searchWorkflowNodes: 'Search Workflow Nodes',
|
searchWorkflowNodes: 'Search Workflow Nodes',
|
||||||
searchWorkflowNodesDesc: 'Find and jump to nodes in the current workflow by name or type',
|
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.',
|
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: {
|
emptyState: {
|
||||||
noAppsFound: 'No apps found',
|
noAppsFound: 'No apps found',
|
||||||
|
@@ -278,6 +278,19 @@ const translation = {
|
|||||||
searchWorkflowNodes: '搜索工作流节点',
|
searchWorkflowNodes: '搜索工作流节点',
|
||||||
searchWorkflowNodesDesc: '按名称或类型查找并跳转到当前工作流中的节点',
|
searchWorkflowNodesDesc: '按名称或类型查找并跳转到当前工作流中的节点',
|
||||||
searchWorkflowNodesHelp: '此功能仅在查看工作流时有效。首先导航到工作流。',
|
searchWorkflowNodesHelp: '此功能仅在查看工作流时有效。首先导航到工作流。',
|
||||||
|
runTitle: '命令',
|
||||||
|
runDesc: '快速执行命令(主题、语言等)',
|
||||||
|
themeCategoryTitle: '主题',
|
||||||
|
themeCategoryDesc: '切换应用主题',
|
||||||
|
themeSystem: '系统主题',
|
||||||
|
themeSystemDesc: '跟随系统外观',
|
||||||
|
themeLight: '浅色主题',
|
||||||
|
themeLightDesc: '使用浅色外观',
|
||||||
|
themeDark: '深色主题',
|
||||||
|
themeDarkDesc: '使用深色外观',
|
||||||
|
languageCategoryTitle: '语言',
|
||||||
|
languageCategoryDesc: '切换界面语言',
|
||||||
|
languageChangeDesc: '更改界面语言',
|
||||||
},
|
},
|
||||||
emptyState: {
|
emptyState: {
|
||||||
noAppsFound: '未找到应用',
|
noAppsFound: '未找到应用',
|
||||||
|
Reference in New Issue
Block a user