erp-platform/ui/src/components/template/Search.tsx

263 lines
7.9 KiB
TypeScript

import Button from '@/components/ui/Button'
import Dialog from '@/components/ui/Dialog'
import { GLOBAL_SEARCH } from '@/constants/permission.constant'
import withHeaderItem from '@/utils/hoc/withHeaderItem'
import useThemeClass from '@/utils/hooks/useThemeClass'
import classNames from 'classnames'
import { useEffect, useRef, useState } from 'react'
import Highlighter from 'react-highlight-words'
import { FaChevronRight, FaSearch } from 'react-icons/fa'
import { Link } from 'react-router-dom'
import { PermissionCheck } from '../shared'
import { Badge, Checkbox, Pagination, Tooltip } from '../ui'
import { useLocalization } from '@/utils/hooks/useLocalization'
import { getSearch, getSystems } from '@/services/global-search.service'
import { FcSearch } from 'react-icons/fc'
type SearchData = {
title: string
url: string
icon?: string
category?: string
categoryTitle?: string
}
type SearchResult = {
title: string
badge?: string
data: SearchData[]
}
const ListItem = (props: {
icon?: string
label: string
url: string
isLast?: boolean
keyWord: string
onNavigate: () => void
}) => {
const { icon, label, url = '', isLast, keyWord, onNavigate } = props
const { textTheme } = useThemeClass()
return (
<Link to={url} onClick={onNavigate}>
<div
className={classNames(
'flex items-center justify-between rounded-lg p-3.5 cursor-pointer user-select',
'bg-gray-50 dark:bg-gray-700/60 hover:bg-gray-100 dark:hover:bg-gray-700/90',
!isLast && 'mb-3',
)}
>
<div className="flex items-center">
{/* <div
className={classNames(
'mr-4 rounded-md ring-1 ring-slate-900/5 shadow-sm text-xl group-hover:shadow h-6 w-6 flex items-center justify-center bg-white dark:bg-gray-700',
textTheme,
'dark:text-gray-100',
)}
>
{icon && navigationIcon[icon]}
</div> */}
<div className="text-gray-900 dark:text-gray-300">
<Highlighter
autoEscape
highlightClassName={classNames(
textTheme,
'underline bg-transparent font-semibold dark:text-white',
)}
searchWords={[keyWord]}
textToHighlight={label}
/>
</div>
</div>
<FaChevronRight className="text-lg" />
</div>
</Link>
)
}
const _Search = ({ className }: { className?: string }) => {
const [searchDialogOpen, setSearchDialogOpen] = useState(false)
const [searchResult, setSearchResult] = useState<SearchResult[]>([])
const [systems, setSystems] = useState<string[]>([])
const [noResult, setNoResult] = useState(false)
const [totalCount, setTotalCount] = useState(0)
const [currentPage, setCurrentPage] = useState(1)
const [currentSystems, setCurrentSystems] = useState<string[]>([])
const { translate } = useLocalization()
const inputRef = useRef<HTMLInputElement>(null)
const handleReset = () => {
setNoResult(false)
setCurrentPage(1)
setTotalCount(0)
setSearchResult([])
}
const handleSearchOpen = async () => {
const result = await getSystems()
if (result?.data) {
setSystems(result.data)
setCurrentSystems(result.data)
}
setSearchDialogOpen(true)
}
const handleSearchClose = () => {
setSearchDialogOpen(false)
if (noResult) {
setTimeout(() => {
handleReset()
}, 300)
}
}
const handleSearch = async () => {
const q = inputRef.current?.value
if (!q || q.length < 3) return
const result = await getSearch({ query: q, system: currentSystems, page: currentPage })
if (!result.data?.totalCount || !result.data.items?.length) {
setNoResult(true)
setTotalCount(0)
setSearchResult([])
return
}
setTotalCount(result.data.totalCount)
const results = result.data.items.reduce((acc, curr) => {
const a = acc.find((item) => item.title === curr.group && item.badge === curr.system)
if (a) {
a.data.push({
title: curr.term,
url: curr.url,
})
} else {
acc.push({
title: curr.group,
badge: curr.system,
data: [
{
title: curr.term,
url: curr.url,
},
],
})
}
return acc
}, [] as SearchResult[])
setSearchResult(results)
}
useEffect(() => {
handleSearch()
}, [currentPage])
useEffect(() => {
if (searchDialogOpen) {
const timeout = setTimeout(() => inputRef.current?.focus(), 100)
return () => {
clearTimeout(timeout)
}
}
}, [searchDialogOpen])
const handleNavigate = () => {
handleSearchClose()
}
return (
<PermissionCheck permissions={[GLOBAL_SEARCH]}>
<Tooltip title={translate('::App.Search')}>
<div className={classNames(className, 'text-2xl')} onClick={handleSearchOpen}>
<FcSearch />
</div>
</Tooltip>
<Dialog
contentClassName="p-0"
isOpen={searchDialogOpen}
closable={false}
onRequestClose={handleSearchClose}
>
<div>
<div className="px-4 flex items-center border-b border-gray-200 dark:border-gray-600">
<div className="flex items-center flex-1">
<FaSearch className="text-xl" />
<input
ref={inputRef}
className="ring-0 outline-none block w-full p-4 text-base bg-transparent text-gray-900 dark:text-gray-100"
placeholder={translate('::App.Search')}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
/>
</div>
<Button size="sm" onClick={handleSearch}>
<FaSearch />
</Button>
</div>
<div>
<div className="flex items-center justify-between p-4 border-b border-gray-200 dark:border-gray-600">
<div className="flex items-center space-x-2">
<span className="text-sm">{translate('::App.SearchIn')}</span>
<Checkbox.Group
value={currentSystems}
onChange={(value) => setCurrentSystems(value as string[])}
>
{systems.map((system) => (
<Checkbox key={system} value={system}>
{system}
</Checkbox>
))}
</Checkbox.Group>
</div>
</div>
</div>
<div className="py-6 px-5 max-h-[550px] overflow-y-auto">
{searchResult.map((result) => (
<div key={`${result.title}-${result.badge}`} className="mb-6">
<div className="flex items-center justify-between mb-1">
<h6 className="mb-3">{result.title}</h6>
<Badge content={result.badge} />
</div>
{result.data.map((data, index) => (
<ListItem
key={data.title + index}
icon={data.icon}
label={data.title}
url={data.url}
keyWord={inputRef.current?.value || ''}
onNavigate={handleNavigate}
/>
))}
</div>
))}
{searchResult.length === 0 && noResult && (
<div className="my-10 text-center text-lg">
<span>{translate('::App.NoResults')}</span>
<div className="heading-text">'{inputRef.current?.value}'</div>
</div>
)}
<Pagination
className="float-right"
total={totalCount}
currentPage={currentPage}
pageSize={10}
onChange={setCurrentPage}
/>
</div>
</div>
</Dialog>
</PermissionCheck>
)
}
const Search = withHeaderItem(_Search)
export default Search