| @@ -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 | |||
| @@ -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> | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -286,6 +286,9 @@ const translation = { | |||
| searchTemporarilyUnavailable: 'جستجو به طور موقت در دسترس نیست', | |||
| servicesUnavailableMessage: 'برخی از سرویس های جستجو ممکن است با مشکل مواجه شوند. یک لحظه دیگر دوباره امتحان کنید.', | |||
| someServicesUnavailable: 'برخی از سرویس های جستجو دردسترس نیستند', | |||
| selectSearchType: 'انتخاب کنید چه چیزی را جستجو کنید', | |||
| commandHint: '@ را برای مرور بر اساس دسته بندی تایپ کنید', | |||
| searchHint: 'شروع به تایپ کنید تا فورا همه چیز را جستجو کنید', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -286,6 +286,9 @@ const translation = { | |||
| searchPlaceholder: 'कमांड के लिए खोजें या टाइप करें @...', | |||
| searchTemporarilyUnavailable: 'खोज अस्थायी रूप से उपलब्ध नहीं है', | |||
| servicesUnavailableMessage: 'कुछ खोज सेवाएँ समस्याओं का सामना कर सकती हैं। थोड़ी देर बाद फिर से प्रयास करें।', | |||
| commandHint: '@ का उपयोग कर श्रेणी के अनुसार ब्राउज़ करें', | |||
| selectSearchType: 'खोजने के लिए क्या चुनें', | |||
| searchHint: 'सब कुछ तुरंत खोजने के लिए टाइप करना शुरू करें', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -265,6 +265,9 @@ const translation = { | |||
| inScope: '{{scope}}s 内', | |||
| clearToSearchAll: '@ をクリアしてすべてを検索', | |||
| useAtForSpecific: '特定のタイプには @ を使用', | |||
| selectSearchType: '検索対象を選択', | |||
| searchHint: '入力を開始してすべてを瞬時に検索', | |||
| commandHint: '@ を入力してカテゴリ別に参照', | |||
| actions: { | |||
| searchApplications: 'アプリケーションを検索', | |||
| searchApplicationsDesc: 'アプリケーションを検索してナビゲート', | |||
| @@ -306,6 +306,9 @@ const translation = { | |||
| searchFailed: '검색 실패', | |||
| searchPlaceholder: '명령을 검색하거나 @를 입력합니다...', | |||
| clearToSearchAll: '@를 지우면 모두 검색됩니다.', | |||
| selectSearchType: '검색할 항목 선택', | |||
| commandHint: '@를 입력하여 카테고리별로 찾아봅니다.', | |||
| searchHint: '즉시 모든 것을 검색하려면 입력을 시작하세요.', | |||
| }, | |||
| } | |||
| @@ -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ć', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -286,6 +286,9 @@ const translation = { | |||
| searchPlaceholder: 'Найдите или введите @ для команд...', | |||
| someServicesUnavailable: 'Некоторые поисковые сервисы недоступны', | |||
| servicesUnavailableMessage: 'В некоторых поисковых службах могут возникать проблемы. Повторите попытку через мгновение.', | |||
| searchHint: 'Начните печатать, чтобы мгновенно искать все', | |||
| commandHint: 'Введите @ для просмотра по категориям', | |||
| selectSearchType: 'Выберите, что искать', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -282,6 +282,9 @@ const translation = { | |||
| searchPlaceholder: 'ค้นหาหรือพิมพ์ @ สําหรับคําสั่ง...', | |||
| servicesUnavailableMessage: 'บริการค้นหาบางบริการอาจประสบปัญหา ลองอีกครั้งในอีกสักครู่', | |||
| searching: 'กำลังค้นหา...', | |||
| searchHint: 'เริ่มพิมพ์เพื่อค้นหาทุกอย่างได้ทันที', | |||
| selectSearchType: 'เลือกสิ่งที่จะค้นหา', | |||
| commandHint: 'พิมพ์ @ เพื่อเรียกดูตามหมวดหมู่', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -286,6 +286,9 @@ const translation = { | |||
| useAtForSpecific: 'Використовуйте @ для конкретних типів', | |||
| someServicesUnavailable: 'Деякі пошукові сервіси недоступні', | |||
| servicesUnavailableMessage: 'У деяких пошукових службах можуть виникати проблеми. Повторіть спробу за мить.', | |||
| selectSearchType: 'Виберіть, що шукати', | |||
| commandHint: 'Введіть @ для навігації за категоріями', | |||
| searchHint: 'Почніть вводити текст, щоб миттєво шукати все', | |||
| }, | |||
| } | |||
| @@ -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', | |||
| }, | |||
| } | |||
| @@ -265,6 +265,9 @@ const translation = { | |||
| inScope: '在 {{scope}}s 中', | |||
| clearToSearchAll: '清除 @ 以搜索全部', | |||
| useAtForSpecific: '使用 @ 进行特定类型搜索', | |||
| selectSearchType: '选择搜索内容', | |||
| searchHint: '开始输入即可立即搜索所有内容', | |||
| commandHint: '输入 @ 按类别浏览', | |||
| actions: { | |||
| searchApplications: '搜索应用程序', | |||
| searchApplicationsDesc: '搜索并导航到您的应用程序', | |||
| @@ -285,6 +285,9 @@ const translation = { | |||
| someServicesUnavailable: '某些搜索服務不可用', | |||
| useAtForSpecific: '對特定類型使用 @', | |||
| searchTemporarilyUnavailable: '搜索暫時不可用', | |||
| selectSearchType: '選擇要搜索的內容', | |||
| commandHint: '鍵入 @ 按類別流覽', | |||
| searchHint: '開始輸入以立即搜索所有內容', | |||
| }, | |||
| } | |||