208 lines
8.1 KiB
TypeScript
208 lines
8.1 KiB
TypeScript
import {ReactElement, SyntheticEvent, useCallback, useEffect, useState} from 'react'
|
|
import {Autocomplete, Box, CircularProgress, Theme, Typography,} from '@mui/material'
|
|
import {makeStyles} from '@mui/styles'
|
|
import TextField from '@mui/material/TextField'
|
|
import {getItem, getItemHierarchy, searchItem} from '../dataaccess/ItemBackend';
|
|
import {ItemOption} from '../dto/ItemOption'
|
|
import {useGlobalState} from '../state/GlobalState'
|
|
// import {useNavigate, useParams} from "react-router-dom";
|
|
import {useRouter, dynamic } from "blitz";
|
|
|
|
let ReactJson
|
|
if (process.browser){
|
|
ReactJson = dynamic(() => import("react-json-view"))
|
|
}
|
|
|
|
interface IItemOption {
|
|
id: string
|
|
name: string
|
|
shortName?: string
|
|
}
|
|
|
|
const useStyles = makeStyles((theme: Theme) => ({
|
|
searchAreaHolder: {
|
|
display: 'flex',
|
|
flexGrow: 1,
|
|
flexDirection: 'column',
|
|
background: theme.palette.background.paper,
|
|
padding: '2vh 2vw 2vh 2vw',
|
|
},
|
|
jsonHolder: {
|
|
display: 'flex',
|
|
flexGrow: 1,
|
|
alignItems: 'center',
|
|
flexDirection: 'column',
|
|
background: theme.palette.background.paper,
|
|
maxHeight: '80vh',
|
|
},
|
|
autocomplete: {},
|
|
}))
|
|
|
|
export const SearchArea = () => {
|
|
const classes = useStyles();
|
|
const router = useRouter()
|
|
const params = router.query;
|
|
const preferedLocale = useGlobalState((state) => state.preferedLocale)
|
|
const preferedJsonViewerTheme = useGlobalState(
|
|
useCallback((state) => state.preferedJsonViewerTheme, []),
|
|
)
|
|
const [searchInputState, setSearchInput] = useGlobalState((state) => [
|
|
state.searchInput,
|
|
state.setSearchInput,
|
|
])
|
|
const [setHierarchy, initHierarchy] = useGlobalState((state) => [state.setHierarchy, state.initHierarchy])
|
|
const [selectedItem, setSelectedItem] = useGlobalState((state) => [
|
|
state.selectedItem,
|
|
state.setSelectedItem,
|
|
])
|
|
const [selectOptions, setSelecteOptions] = useState<ItemOption[]>([])
|
|
const [isbusy, setIsBusy] = useState<boolean>(false)
|
|
const searchThreshold = 3
|
|
|
|
const handleNameInput = useCallback(async (input: string) => {
|
|
const searchResults = await searchItem(input, preferedLocale)
|
|
const options = searchResults?.map((res) => ({
|
|
id: res.item._id,
|
|
name: res.locale.Name ? res.locale.Name : res.item._name,
|
|
shortName: JSON.stringify(res.locale.ShortName)
|
|
}))
|
|
setSelecteOptions(options ? options : [])
|
|
}, [preferedLocale])
|
|
|
|
const handleIDInput = useCallback(async (input: string) => {
|
|
const itemJson = await getItem(input, preferedLocale)
|
|
if (!itemJson) {
|
|
setSelectedItem(undefined)
|
|
setSearchInput('')
|
|
return;
|
|
}
|
|
|
|
setSelectedItem(itemJson)
|
|
const itemObj = {
|
|
id: itemJson.item._id,
|
|
name: itemJson.locale.Name ? itemJson.locale.Name : itemJson.item._name,
|
|
shortName: itemJson.locale.ShortName
|
|
}
|
|
setSelecteOptions([itemObj])
|
|
setSearchInput(itemObj.name)
|
|
|
|
// Update hierachy
|
|
const itemHierarchy = await getItemHierarchy(
|
|
itemJson.item,
|
|
preferedLocale,
|
|
)
|
|
setHierarchy(itemHierarchy ? itemHierarchy : {})
|
|
// eslint-disable-next-line
|
|
}, []) // Need to only be created on startup
|
|
|
|
useEffect(() => initHierarchy(), [initHierarchy])
|
|
|
|
useEffect(()=>{
|
|
if (selectedItem){
|
|
router.replace(`/search/${selectedItem.item._id}`)
|
|
}
|
|
},[selectedItem, router])
|
|
|
|
useEffect(() => {
|
|
if (searchInputState && searchInputState.match(/([a-z0-9]{24})/)) {
|
|
handleIDInput(searchInputState)
|
|
}
|
|
}, [handleIDInput, searchInputState])
|
|
|
|
const handleInput = useCallback(async (input: string) => {
|
|
if (!input || input.length < searchThreshold || isbusy) {
|
|
setSelectedItem(undefined)
|
|
setSelecteOptions([])
|
|
setIsBusy(false)
|
|
return
|
|
}
|
|
setIsBusy(true)
|
|
|
|
if (input.match(/([a-z0-9]{24})/)) await handleIDInput(input)
|
|
else await handleNameInput(input)
|
|
|
|
setIsBusy(false)
|
|
}, [handleIDInput, handleNameInput, isbusy, setSelectedItem])
|
|
|
|
useEffect(() => {
|
|
if (!searchInputState && params.id) {
|
|
const newId = (params.id as string).trim();
|
|
console.log(newId);
|
|
setSearchInput(newId);
|
|
(async () => await handleInput(newId))();
|
|
}
|
|
}, [params, searchInputState, setSearchInput, handleInput, router])
|
|
|
|
const formatDisplayItems = () => {
|
|
// If loading
|
|
if (isbusy) return <CircularProgress size={100}/>
|
|
|
|
// If finished loading
|
|
console.log(process.browser)
|
|
if (selectedItem && ReactJson !== undefined) {
|
|
return (<ReactJson
|
|
src={selectedItem!}
|
|
theme={preferedJsonViewerTheme}
|
|
style={{
|
|
marginTop: '2vh',
|
|
width: '100%',
|
|
overflowY: 'auto',
|
|
display: 'flex',
|
|
}}
|
|
/>)
|
|
} else return <Typography id='search-no-data'>No data to display</Typography>
|
|
// return <Typography id='search-no-data'>No data to display</Typography>
|
|
}
|
|
|
|
const findOptionValue = (option: ItemOption, value: ItemOption): boolean => {
|
|
return option.name?.toLocaleLowerCase() === value.name?.toLocaleLowerCase()
|
|
|| option.id?.toLocaleLowerCase() === value.id?.toLocaleLowerCase()
|
|
|| option.shortName?.toLocaleLowerCase() === value.shortName?.toLocaleLowerCase()
|
|
}
|
|
|
|
return (
|
|
<Box className={classes.searchAreaHolder}>
|
|
<Autocomplete
|
|
id='search-autocomplete'
|
|
options={selectOptions.map((elt) => ({name: elt.name, shortName: elt.shortName, id: elt.id}))}
|
|
getOptionLabel={(option) => (option.name ? option.name : option.id) as string}
|
|
isOptionEqualToValue={(option, value) => findOptionValue(option, value)}
|
|
open={!isbusy && searchInputState.length >= searchThreshold && (searchInputState !== selectedItem?.locale.Name && searchInputState !== selectedItem?.item._name)}
|
|
className={classes.autocomplete}
|
|
inputValue={searchInputState ? searchInputState : ''}
|
|
onInputChange={async (evt: SyntheticEvent, newValue: string) => {
|
|
if (!evt) return
|
|
setSelectedItem(undefined)
|
|
setSearchInput(newValue)
|
|
await handleInput(newValue.trim())
|
|
}}
|
|
value={(() => {
|
|
const selectedOption = selectOptions.find(elt => elt.id === searchInputState || elt.name === searchInputState);
|
|
return selectedOption ? selectedOption : null;
|
|
})()}
|
|
onChange={async (event: SyntheticEvent, newValue: IItemOption | null) => {
|
|
if (newValue) {
|
|
const selectedOption = selectOptions.find(
|
|
(elt) => elt.name === newValue.name,
|
|
)
|
|
if (selectedOption) await handleIDInput(selectedOption.id)
|
|
}
|
|
}}
|
|
renderInput={(params) => (
|
|
<TextField {...params} label="Search by name or ID"/>
|
|
)}
|
|
renderOption={(props, option ) => (
|
|
<li {...props} key={option.id}><Typography>{option.name}</Typography></li>
|
|
)}
|
|
filterOptions={(options, state) => options.filter(elt => {
|
|
return (elt.name?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase())
|
|
|| elt.id?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase())
|
|
|| elt.shortName?.toLocaleLowerCase().includes(state.inputValue.toLocaleLowerCase()))
|
|
})}
|
|
filterSelectedOptions
|
|
/>
|
|
<Box className={classes.jsonHolder}>{formatDisplayItems()}</Box>
|
|
</Box>
|
|
)
|
|
}
|