feat: enhance GotoAnything UX with @ command selector (#23738)

This commit is contained in:
lyzno1
2025-08-11 15:47:19 +08:00
committed by GitHub
parent 43411d7a9e
commit 2c81db5a1c
22 changed files with 145 additions and 26 deletions

View File

@@ -0,0 +1,51 @@
import type { FC } from 'react'
import { Command } from 'cmdk'
import { useTranslation } from 'react-i18next'
import type { ActionItem } from './actions/types'
type Props = {
actions: Record<string, ActionItem>
onCommandSelect: (commandKey: string) => void
}
const CommandSelector: FC<Props> = ({ actions, onCommandSelect }) => {
const { t } = useTranslation()
return (
<div className="p-4">
<div className="mb-3 text-left text-sm font-medium text-text-secondary">
{t('app.gotoAnything.selectSearchType')}
</div>
<Command.Group className="space-y-1">
{Object.values(actions).map(action => (
<Command.Item
key={action.key}
value={action.shortcut}
className="flex cursor-pointer items-center rounded-md
p-2.5
transition-all
duration-150 hover:bg-state-base-hover aria-[selected=true]:bg-state-base-hover"
onSelect={() => onCommandSelect(action.shortcut)}
>
<span className="min-w-[4.5rem] text-left font-mono text-xs text-text-tertiary">
{action.shortcut}
</span>
<span className="ml-3 text-sm text-text-secondary">
{(() => {
const keyMap: Record<string, string> = {
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
}
return t(keyMap[action.key])
})()}
</span>
</Command.Item>
))}
</Command.Group>
</div>
)
}
export default CommandSelector

View File

@@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace' import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
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'
type Props = { type Props = {
onHide?: () => void onHide?: () => void
@@ -81,11 +82,15 @@ const GotoAnything: FC<Props> = ({
wait: 300, wait: 300,
}) })
const isCommandsMode = searchQuery.trim() === '@'
const searchMode = useMemo(() => { const searchMode = useMemo(() => {
if (isCommandsMode) return 'commands'
const query = searchQueryDebouncedValue.toLowerCase() const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions) const action = matchAction(query, Actions)
return action ? action.key : 'general' return action ? action.key : 'general'
}, [searchQueryDebouncedValue, Actions]) }, [searchQueryDebouncedValue, Actions, isCommandsMode])
const { data: searchResults = [], isLoading, isError, error } = useQuery( const { data: searchResults = [], isLoading, isError, error } = useQuery(
{ {
@@ -103,12 +108,20 @@ const GotoAnything: FC<Props> = ({
const action = matchAction(query, Actions) const action = matchAction(query, Actions)
return await searchAnything(defaultLocale, query, action) return await searchAnything(defaultLocale, query, action)
}, },
enabled: !!searchQueryDebouncedValue, enabled: !!searchQueryDebouncedValue && !isCommandsMode,
staleTime: 30000, staleTime: 30000,
gcTime: 300000, gcTime: 300000,
}, },
) )
const handleCommandSelect = useCallback((commandKey: string) => {
setSearchQuery(`${commandKey} `)
setCmdVal('')
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}, [])
// Handle navigation to selected result // Handle navigation to selected result
const handleNavigate = useCallback((result: SearchResult) => { const handleNavigate = useCallback((result: SearchResult) => {
setShow(false) setShow(false)
@@ -141,7 +154,7 @@ const GotoAnything: FC<Props> = ({
[searchResults]) [searchResults])
const emptyResult = useMemo(() => { const emptyResult = useMemo(() => {
if (searchResults.length || !searchQueryDebouncedValue.trim() || isLoading) if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
return null return null
const isCommandSearch = searchMode !== 'general' const isCommandSearch = searchMode !== 'general'
@@ -186,34 +199,22 @@ const GotoAnything: FC<Props> = ({
</div> </div>
</div> </div>
) )
}, [searchResults, searchQueryDebouncedValue, Actions, searchMode, isLoading, isError]) }, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])
const defaultUI = useMemo(() => { const defaultUI = useMemo(() => {
if (searchQueryDebouncedValue.trim()) if (searchQuery.trim())
return null return null
return (<div className="flex items-center justify-center py-8 text-center text-text-tertiary"> return (<div className="flex items-center justify-center py-12 text-center text-text-tertiary">
<div> <div>
<div className='text-sm font-medium'>{t('app.gotoAnything.searchTitle')}</div> <div className='text-sm font-medium'>{t('app.gotoAnything.searchTitle')}</div>
<div className='mt-3 space-y-2 text-xs text-text-quaternary'> <div className='mt-3 space-y-1 text-xs text-text-quaternary'>
{Object.values(Actions).map(action => ( <div>{t('app.gotoAnything.searchHint')}</div>
<div key={action.key} className='flex items-center gap-2'> <div>{t('app.gotoAnything.commandHint')}</div>
<span className='inline-flex items-center rounded bg-gray-200 px-2 py-1 font-mono text-xs font-medium text-gray-600 dark:bg-gray-700 dark:text-gray-200'>{action.shortcut}</span>
<span>{(() => {
const keyMap: Record<string, string> = {
'@app': 'app.gotoAnything.actions.searchApplicationsDesc',
'@plugin': 'app.gotoAnything.actions.searchPluginsDesc',
'@knowledge': 'app.gotoAnything.actions.searchKnowledgeBasesDesc',
'@node': 'app.gotoAnything.actions.searchWorkflowNodesDesc',
}
return t(keyMap[action.key])
})()}</span>
</div>
))}
</div> </div>
</div> </div>
</div>) </div>)
}, [searchQueryDebouncedValue, Actions]) }, [searchQuery, Actions])
useEffect(() => { useEffect(() => {
if (show) { if (show) {
@@ -296,7 +297,13 @@ const GotoAnything: FC<Props> = ({
)} )}
{!isLoading && !isError && ( {!isLoading && !isError && (
<> <>
{Object.entries(groupedResults).map(([type, results], groupIndex) => ( {isCommandsMode ? (
<CommandSelector
actions={Actions}
onCommandSelect={handleCommandSelect}
/>
) : (
Object.entries(groupedResults).map(([type, results], groupIndex) => (
<Command.Group key={groupIndex} heading={(() => { <Command.Group key={groupIndex} heading={(() => {
const typeMap: Record<string, string> = { const typeMap: Record<string, string> = {
'app': 'app.gotoAnything.groups.apps', 'app': 'app.gotoAnything.groups.apps',
@@ -330,9 +337,10 @@ const GotoAnything: FC<Props> = ({
</Command.Item> </Command.Item>
))} ))}
</Command.Group> </Command.Group>
))} ))
{emptyResult} )}
{defaultUI} {!isCommandsMode && emptyResult}
{!isCommandsMode && defaultUI}
</> </>
)} )}
</Command.List> </Command.List>

View File

@@ -288,6 +288,9 @@ const translation = {
useAtForSpecific: 'Verwenden von @ für bestimmte Typen', useAtForSpecific: 'Verwenden von @ für bestimmte Typen',
searchTitle: 'Suchen Sie nach irgendetwas', searchTitle: 'Suchen Sie nach irgendetwas',
searching: 'Suche...', searching: 'Suche...',
selectSearchType: 'Wählen Sie aus, wonach gesucht werden soll',
commandHint: 'Geben Sie @ ein, um nach Kategorie zu suchen',
searchHint: 'Beginnen Sie mit der Eingabe, um alles sofort zu durchsuchen',
}, },
} }

View File

@@ -266,6 +266,9 @@ const translation = {
inScope: 'in {{scope}}s', inScope: 'in {{scope}}s',
clearToSearchAll: 'Clear @ to search all', clearToSearchAll: 'Clear @ to search all',
useAtForSpecific: 'Use @ for specific types', useAtForSpecific: 'Use @ for specific types',
selectSearchType: 'Choose what to search for',
searchHint: 'Start typing to search everything instantly',
commandHint: 'Type @ to browse by category',
actions: { actions: {
searchApplications: 'Search Applications', searchApplications: 'Search Applications',
searchApplicationsDesc: 'Search and navigate to your applications', searchApplicationsDesc: 'Search and navigate to your applications',

View File

@@ -286,6 +286,9 @@ const translation = {
searchTitle: 'Busca cualquier cosa', searchTitle: 'Busca cualquier cosa',
someServicesUnavailable: 'Algunos servicios de búsqueda no están disponibles', someServicesUnavailable: 'Algunos servicios de búsqueda no están disponibles',
servicesUnavailableMessage: 'Algunos servicios de búsqueda pueden estar experimentando problemas. Inténtalo de nuevo en un momento.', servicesUnavailableMessage: 'Algunos servicios de búsqueda pueden estar experimentando problemas. Inténtalo de nuevo en un momento.',
searchHint: 'Empieza a escribir para buscar todo al instante',
commandHint: 'Escriba @ para buscar por categoría',
selectSearchType: 'Elige qué buscar',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
searchTemporarilyUnavailable: 'جستجو به طور موقت در دسترس نیست', searchTemporarilyUnavailable: 'جستجو به طور موقت در دسترس نیست',
servicesUnavailableMessage: 'برخی از سرویس های جستجو ممکن است با مشکل مواجه شوند. یک لحظه دیگر دوباره امتحان کنید.', servicesUnavailableMessage: 'برخی از سرویس های جستجو ممکن است با مشکل مواجه شوند. یک لحظه دیگر دوباره امتحان کنید.',
someServicesUnavailable: 'برخی از سرویس های جستجو دردسترس نیستند', someServicesUnavailable: 'برخی از سرویس های جستجو دردسترس نیستند',
selectSearchType: 'انتخاب کنید چه چیزی را جستجو کنید',
commandHint: '@ را برای مرور بر اساس دسته بندی تایپ کنید',
searchHint: 'شروع به تایپ کنید تا فورا همه چیز را جستجو کنید',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
searchPlaceholder: 'Recherchez ou tapez @ pour les commandes...', searchPlaceholder: 'Recherchez ou tapez @ pour les commandes...',
searchFailed: 'Echec de la recherche', searchFailed: 'Echec de la recherche',
noResults: 'Aucun résultat trouvé', noResults: 'Aucun résultat trouvé',
commandHint: 'Tapez @ pour parcourir par catégorie',
selectSearchType: 'Choisissez les éléments de recherche',
searchHint: 'Commencez à taper pour tout rechercher instantanément',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
searchPlaceholder: 'कमांड के लिए खोजें या टाइप करें @...', searchPlaceholder: 'कमांड के लिए खोजें या टाइप करें @...',
searchTemporarilyUnavailable: 'खोज अस्थायी रूप से उपलब्ध नहीं है', searchTemporarilyUnavailable: 'खोज अस्थायी रूप से उपलब्ध नहीं है',
servicesUnavailableMessage: 'कुछ खोज सेवाएँ समस्याओं का सामना कर सकती हैं। थोड़ी देर बाद फिर से प्रयास करें।', servicesUnavailableMessage: 'कुछ खोज सेवाएँ समस्याओं का सामना कर सकती हैं। थोड़ी देर बाद फिर से प्रयास करें।',
commandHint: '@ का उपयोग कर श्रेणी के अनुसार ब्राउज़ करें',
selectSearchType: 'खोजने के लिए क्या चुनें',
searchHint: 'सब कुछ तुरंत खोजने के लिए टाइप करना शुरू करें',
}, },
} }

View File

@@ -292,6 +292,9 @@ const translation = {
noResults: 'Nessun risultato trovato', noResults: 'Nessun risultato trovato',
useAtForSpecific: 'Utilizzare @ per tipi specifici', useAtForSpecific: 'Utilizzare @ per tipi specifici',
clearToSearchAll: 'Cancella @ per cercare tutto', clearToSearchAll: 'Cancella @ per cercare tutto',
selectSearchType: 'Scegli cosa cercare',
commandHint: 'Digita @ per sfogliare per categoria',
searchHint: 'Inizia a digitare per cercare tutto all\'istante',
}, },
} }

View File

@@ -265,6 +265,9 @@ const translation = {
inScope: '{{scope}}s 内', inScope: '{{scope}}s 内',
clearToSearchAll: '@ をクリアしてすべてを検索', clearToSearchAll: '@ をクリアしてすべてを検索',
useAtForSpecific: '特定のタイプには @ を使用', useAtForSpecific: '特定のタイプには @ を使用',
selectSearchType: '検索対象を選択',
searchHint: '入力を開始してすべてを瞬時に検索',
commandHint: '@ を入力してカテゴリ別に参照',
actions: { actions: {
searchApplications: 'アプリケーションを検索', searchApplications: 'アプリケーションを検索',
searchApplicationsDesc: 'アプリケーションを検索してナビゲート', searchApplicationsDesc: 'アプリケーションを検索してナビゲート',

View File

@@ -306,6 +306,9 @@ const translation = {
searchFailed: '검색 실패', searchFailed: '검색 실패',
searchPlaceholder: '명령을 검색하거나 @를 입력합니다...', searchPlaceholder: '명령을 검색하거나 @를 입력합니다...',
clearToSearchAll: '@를 지우면 모두 검색됩니다.', clearToSearchAll: '@를 지우면 모두 검색됩니다.',
selectSearchType: '검색할 항목 선택',
commandHint: '@를 입력하여 카테고리별로 찾아봅니다.',
searchHint: '즉시 모든 것을 검색하려면 입력을 시작하세요.',
}, },
} }

View File

@@ -287,6 +287,9 @@ const translation = {
searchTemporarilyUnavailable: 'Wyszukiwanie chwilowo niedostępne', searchTemporarilyUnavailable: 'Wyszukiwanie chwilowo niedostępne',
servicesUnavailableMessage: 'W przypadku niektórych usług wyszukiwania mogą występować problemy. Spróbuj ponownie za chwilę.', servicesUnavailableMessage: 'W przypadku niektórych usług wyszukiwania mogą występować problemy. Spróbuj ponownie za chwilę.',
searchFailed: 'Wyszukiwanie nie powiodło się', searchFailed: 'Wyszukiwanie nie powiodło się',
searchHint: 'Zacznij pisać, aby natychmiast wszystko przeszukać',
commandHint: 'Wpisz @, aby przeglądać według kategorii',
selectSearchType: 'Wybierz, czego chcesz szukać',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
useAtForSpecific: 'Use @ para tipos específicos', useAtForSpecific: 'Use @ para tipos específicos',
clearToSearchAll: 'Desmarque @ para pesquisar tudo', clearToSearchAll: 'Desmarque @ para pesquisar tudo',
searchFailed: 'Falha na pesquisa', searchFailed: 'Falha na pesquisa',
searchHint: 'Comece a digitar para pesquisar tudo instantaneamente',
commandHint: 'Digite @ para navegar por categoria',
selectSearchType: 'Escolha o que pesquisar',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
servicesUnavailableMessage: 'Este posibil ca unele servicii de căutare să întâmpine probleme. Încercați din nou într-o clipă.', servicesUnavailableMessage: 'Este posibil ca unele servicii de căutare să întâmpine probleme. Încercați din nou într-o clipă.',
someServicesUnavailable: 'Unele servicii de căutare nu sunt disponibile', someServicesUnavailable: 'Unele servicii de căutare nu sunt disponibile',
clearToSearchAll: 'Ștergeți @ pentru a căuta toate', clearToSearchAll: 'Ștergeți @ pentru a căuta toate',
selectSearchType: 'Alegeți ce să căutați',
commandHint: 'Tastați @ pentru a naviga după categorie',
searchHint: 'Începeți să tastați pentru a căuta totul instantaneu',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
searchPlaceholder: 'Найдите или введите @ для команд...', searchPlaceholder: 'Найдите или введите @ для команд...',
someServicesUnavailable: 'Некоторые поисковые сервисы недоступны', someServicesUnavailable: 'Некоторые поисковые сервисы недоступны',
servicesUnavailableMessage: 'В некоторых поисковых службах могут возникать проблемы. Повторите попытку через мгновение.', servicesUnavailableMessage: 'В некоторых поисковых службах могут возникать проблемы. Повторите попытку через мгновение.',
searchHint: 'Начните печатать, чтобы мгновенно искать все',
commandHint: 'Введите @ для просмотра по категориям',
selectSearchType: 'Выберите, что искать',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
searchFailed: 'Iskanje ni uspelo', searchFailed: 'Iskanje ni uspelo',
useAtForSpecific: 'Uporaba znaka @ za določene vrste', useAtForSpecific: 'Uporaba znaka @ za določene vrste',
servicesUnavailableMessage: 'Pri nekaterih iskalnih storitvah se morda pojavljajo težave. Poskusite znova čez trenutek.', servicesUnavailableMessage: 'Pri nekaterih iskalnih storitvah se morda pojavljajo težave. Poskusite znova čez trenutek.',
commandHint: 'Vnesite @ za brskanje po kategoriji',
selectSearchType: 'Izberite, kaj želite iskati',
searchHint: 'Začnite tipkati, da takoj preiščete vse',
}, },
} }

View File

@@ -282,6 +282,9 @@ const translation = {
searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สําหรับคําสั่ง...', searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สําหรับคําสั่ง...',
servicesUnavailableMessage: 'บริการค้นหาบางบริการอาจประสบปัญหา ลองอีกครั้งในอีกสักครู่', servicesUnavailableMessage: 'บริการค้นหาบางบริการอาจประสบปัญหา ลองอีกครั้งในอีกสักครู่',
searching: 'กำลังค้นหา...', searching: 'กำลังค้นหา...',
searchHint: 'เริ่มพิมพ์เพื่อค้นหาทุกอย่างได้ทันที',
selectSearchType: 'เลือกสิ่งที่จะค้นหา',
commandHint: 'พิมพ์ @ เพื่อเรียกดูตามหมวดหมู่',
}, },
} }

View File

@@ -282,6 +282,9 @@ const translation = {
noResults: 'Sonuç bulunamadı', noResults: 'Sonuç bulunamadı',
servicesUnavailableMessage: 'Bazı arama hizmetlerinde sorunlar yaşanıyor olabilir. Kısa bir süre sonra tekrar deneyin.', servicesUnavailableMessage: 'Bazı arama hizmetlerinde sorunlar yaşanıyor olabilir. Kısa bir süre sonra tekrar deneyin.',
searching: 'Araştırıcı...', searching: 'Araştırıcı...',
selectSearchType: 'Ne arayacağınızı seçin',
searchHint: 'Her şeyi anında aramak için yazmaya başlayın',
commandHint: 'Kategoriye göre göz atmak için @ yazın',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
useAtForSpecific: 'Використовуйте @ для конкретних типів', useAtForSpecific: 'Використовуйте @ для конкретних типів',
someServicesUnavailable: 'Деякі пошукові сервіси недоступні', someServicesUnavailable: 'Деякі пошукові сервіси недоступні',
servicesUnavailableMessage: 'У деяких пошукових службах можуть виникати проблеми. Повторіть спробу за мить.', servicesUnavailableMessage: 'У деяких пошукових службах можуть виникати проблеми. Повторіть спробу за мить.',
selectSearchType: 'Виберіть, що шукати',
commandHint: 'Введіть @ для навігації за категоріями',
searchHint: 'Почніть вводити текст, щоб миттєво шукати все',
}, },
} }

View File

@@ -286,6 +286,9 @@ const translation = {
useAtForSpecific: 'Sử dụng @ cho các loại cụ thể', useAtForSpecific: 'Sử dụng @ cho các loại cụ thể',
someServicesUnavailable: 'Một số dịch vụ tìm kiếm không khả dụng', someServicesUnavailable: 'Một số dịch vụ tìm kiếm không khả dụng',
servicesUnavailableMessage: 'Một số dịch vụ tìm kiếm có thể gặp sự cố. Thử lại trong giây lát.', servicesUnavailableMessage: 'Một số dịch vụ tìm kiếm có thể gặp sự cố. Thử lại trong giây lát.',
searchHint: 'Bắt đầu nhập để tìm kiếm mọi thứ ngay lập tức',
commandHint: 'Nhập @ để duyệt theo danh mục',
selectSearchType: 'Chọn nội dung để tìm kiếm',
}, },
} }

View File

@@ -265,6 +265,9 @@ const translation = {
inScope: '在 {{scope}}s 中', inScope: '在 {{scope}}s 中',
clearToSearchAll: '清除 @ 以搜索全部', clearToSearchAll: '清除 @ 以搜索全部',
useAtForSpecific: '使用 @ 进行特定类型搜索', useAtForSpecific: '使用 @ 进行特定类型搜索',
selectSearchType: '选择搜索内容',
searchHint: '开始输入即可立即搜索所有内容',
commandHint: '输入 @ 按类别浏览',
actions: { actions: {
searchApplications: '搜索应用程序', searchApplications: '搜索应用程序',
searchApplicationsDesc: '搜索并导航到您的应用程序', searchApplicationsDesc: '搜索并导航到您的应用程序',

View File

@@ -285,6 +285,9 @@ const translation = {
someServicesUnavailable: '某些搜索服務不可用', someServicesUnavailable: '某些搜索服務不可用',
useAtForSpecific: '對特定類型使用 @', useAtForSpecific: '對特定類型使用 @',
searchTemporarilyUnavailable: '搜索暫時不可用', searchTemporarilyUnavailable: '搜索暫時不可用',
selectSearchType: '選擇要搜索的內容',
commandHint: '鍵入 @ 按類別流覽',
searchHint: '開始輸入以立即搜索所有內容',
}, },
} }