소스 검색

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

tags/1.7.2
lyzno1 2 달 전
부모
커밋
2c81db5a1c
No account linked to committer's email address

+ 51
- 0
web/app/components/goto-anything/command-selector.tsx 파일 보기

@@ -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

+ 34
- 26
web/app/components/goto-anything/index.tsx 파일 보기

@@ -17,6 +17,7 @@ import { useTranslation } from 'react-i18next'
import InstallFromMarketplace from '../plugins/install-plugin/install-from-marketplace'
import type { Plugin } from '../plugins/types'
import { Command } from 'cmdk'
import CommandSelector from './command-selector'

type Props = {
onHide?: () => void
@@ -81,11 +82,15 @@ const GotoAnything: FC<Props> = ({
wait: 300,
})

const isCommandsMode = searchQuery.trim() === '@'

const searchMode = useMemo(() => {
if (isCommandsMode) return 'commands'

const query = searchQueryDebouncedValue.toLowerCase()
const action = matchAction(query, Actions)
return action ? action.key : 'general'
}, [searchQueryDebouncedValue, Actions])
}, [searchQueryDebouncedValue, Actions, isCommandsMode])

const { data: searchResults = [], isLoading, isError, error } = useQuery(
{
@@ -103,12 +108,20 @@ const GotoAnything: FC<Props> = ({
const action = matchAction(query, Actions)
return await searchAnything(defaultLocale, query, action)
},
enabled: !!searchQueryDebouncedValue,
enabled: !!searchQueryDebouncedValue && !isCommandsMode,
staleTime: 30000,
gcTime: 300000,
},
)

const handleCommandSelect = useCallback((commandKey: string) => {
setSearchQuery(`${commandKey} `)
setCmdVal('')
setTimeout(() => {
inputRef.current?.focus()
}, 0)
}, [])

// Handle navigation to selected result
const handleNavigate = useCallback((result: SearchResult) => {
setShow(false)
@@ -141,7 +154,7 @@ const GotoAnything: FC<Props> = ({
[searchResults])

const emptyResult = useMemo(() => {
if (searchResults.length || !searchQueryDebouncedValue.trim() || isLoading)
if (searchResults.length || !searchQuery.trim() || isLoading || isCommandsMode)
return null

const isCommandSearch = searchMode !== 'general'
@@ -186,34 +199,22 @@ const GotoAnything: FC<Props> = ({
</div>
</div>
)
}, [searchResults, searchQueryDebouncedValue, Actions, searchMode, isLoading, isError])
}, [searchResults, searchQuery, Actions, searchMode, isLoading, isError, isCommandsMode])

const defaultUI = useMemo(() => {
if (searchQueryDebouncedValue.trim())
if (searchQuery.trim())
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 className='text-sm font-medium'>{t('app.gotoAnything.searchTitle')}</div>
<div className='mt-3 space-y-2 text-xs text-text-quaternary'>
{Object.values(Actions).map(action => (
<div key={action.key} className='flex items-center gap-2'>
<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 className='mt-3 space-y-1 text-xs text-text-quaternary'>
<div>{t('app.gotoAnything.searchHint')}</div>
<div>{t('app.gotoAnything.commandHint')}</div>
</div>
</div>
</div>)
}, [searchQueryDebouncedValue, Actions])
}, [searchQuery, Actions])

useEffect(() => {
if (show) {
@@ -296,7 +297,13 @@ const GotoAnything: FC<Props> = ({
)}
{!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={(() => {
const typeMap: Record<string, string> = {
'app': 'app.gotoAnything.groups.apps',
@@ -330,9 +337,10 @@ const GotoAnything: FC<Props> = ({
</Command.Item>
))}
</Command.Group>
))}
{emptyResult}
{defaultUI}
))
)}
{!isCommandsMode && emptyResult}
{!isCommandsMode && defaultUI}
</>
)}
</Command.List>

+ 3
- 0
web/i18n/de-DE/app.ts 파일 보기

@@ -288,6 +288,9 @@ const translation = {
useAtForSpecific: 'Verwenden von @ für bestimmte Typen',
searchTitle: 'Suchen Sie nach irgendetwas',
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',
},
}


+ 3
- 0
web/i18n/en-US/app.ts 파일 보기

@@ -266,6 +266,9 @@ const translation = {
inScope: 'in {{scope}}s',
clearToSearchAll: 'Clear @ to search all',
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: {
searchApplications: 'Search Applications',
searchApplicationsDesc: 'Search and navigate to your applications',

+ 3
- 0
web/i18n/es-ES/app.ts 파일 보기

@@ -286,6 +286,9 @@ const translation = {
searchTitle: 'Busca cualquier cosa',
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.',
searchHint: 'Empieza a escribir para buscar todo al instante',
commandHint: 'Escriba @ para buscar por categoría',
selectSearchType: 'Elige qué buscar',
},
}


+ 3
- 0
web/i18n/fa-IR/app.ts 파일 보기

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


+ 3
- 0
web/i18n/fr-FR/app.ts 파일 보기

@@ -286,6 +286,9 @@ const translation = {
searchPlaceholder: 'Recherchez ou tapez @ pour les commandes...',
searchFailed: 'Echec de la recherche',
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',
},
}


+ 3
- 0
web/i18n/hi-IN/app.ts 파일 보기

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


+ 3
- 0
web/i18n/it-IT/app.ts 파일 보기

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


+ 3
- 0
web/i18n/ja-JP/app.ts 파일 보기

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

+ 3
- 0
web/i18n/ko-KR/app.ts 파일 보기

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


+ 3
- 0
web/i18n/pl-PL/app.ts 파일 보기

@@ -287,6 +287,9 @@ const translation = {
searchTemporarilyUnavailable: 'Wyszukiwanie chwilowo niedostępne',
servicesUnavailableMessage: 'W przypadku niektórych usług wyszukiwania mogą występować problemy. Spróbuj ponownie za chwilę.',
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ć',
},
}


+ 3
- 0
web/i18n/pt-BR/app.ts 파일 보기

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


+ 3
- 0
web/i18n/ro-RO/app.ts 파일 보기

@@ -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ă.',
someServicesUnavailable: 'Unele servicii de căutare nu sunt disponibile',
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',
},
}


+ 3
- 0
web/i18n/ru-RU/app.ts 파일 보기

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


+ 3
- 0
web/i18n/sl-SI/app.ts 파일 보기

@@ -286,6 +286,9 @@ const translation = {
searchFailed: 'Iskanje ni uspelo',
useAtForSpecific: 'Uporaba znaka @ za določene vrste',
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',
},
}


+ 3
- 0
web/i18n/th-TH/app.ts 파일 보기

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


+ 3
- 0
web/i18n/tr-TR/app.ts 파일 보기

@@ -282,6 +282,9 @@ const translation = {
noResults: 'Sonuç bulunamadı',
servicesUnavailableMessage: 'Bazı arama hizmetlerinde sorunlar yaşanıyor olabilir. Kısa bir süre sonra tekrar deneyin.',
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',
},
}


+ 3
- 0
web/i18n/uk-UA/app.ts 파일 보기

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


+ 3
- 0
web/i18n/vi-VN/app.ts 파일 보기

@@ -286,6 +286,9 @@ const translation = {
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',
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',
},
}


+ 3
- 0
web/i18n/zh-Hans/app.ts 파일 보기

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

+ 3
- 0
web/i18n/zh-Hant/app.ts 파일 보기

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


Loading…
취소
저장