mirror of https://github.com/langgenius/dify.git
feat: enhance GotoAnything UX with @ command selector (#23738)
This commit is contained in:
parent
43411d7a9e
commit
2c81db5a1c
|
|
@ -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: '開始輸入以立即搜索所有內容',
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue