frontend/src/common/SearchSelect.tsx

205 lines
5.8 KiB
TypeScript

import { Autocomplete, Avatar, Box, CircularProgress, createFilterOptions } from "@mui/material";
import TextField from "@mui/material/TextField";
import { makeStyles } from "@mui/styles";
import { isArray } from "lodash";
import React, { CSSProperties } from "react";
const useStyles = makeStyles(() => ({
input: {
minWidth: `0 !important`
}})
);
export interface Option {
label: string;
value: any;
group?: string;
new?: boolean;
icon?: string;
defaultCableID?: number;
}
interface Props {
selected?: Option | Option[] | null;
changeSelection?: (newSelected: Option | null) => void;
changeMulti?: (newSelected: Option[] | null) => void;
options: Option[];
label?: string;
group?: boolean;
creatable?: boolean;
loading?: boolean;
disabled?: boolean;
multiple?: boolean;
variant?: "filled" | "outlined" | "standard";
style?: CSSProperties;
getOptionSelected?: any;
onChange?: (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => void;
onKeyDown?: (e: React.KeyboardEvent<HTMLDivElement>) => void;
}
export default function SearchSelect(props: Props) {
const {
options,
changeSelection,
changeMulti,
selected,
label,
creatable,
loading,
group,
disabled,
multiple,
variant,
style,
// getOptionSelected,
onChange,
onKeyDown
} = props;
const classes = useStyles();
const filter = createFilterOptions<Option>({
stringify: (option: Option) => option.label + option.group
});
const stringToOption = (s: string): Option => {
return { value: s, label: s } as Option;
};
// const anyToLabel = (o: any): string => {
// if (o.text && o.value) {
// return o.text + " - " + o.value
// } else if (o.value) {
// return o.value
// } else if (o.label) {
// return o.label
// }
// return o.toString();
// };
const optionSelected = (newValue: string | Option | (string | Option)[] | null) => {
if (multiple === true) {
if (changeMulti) {
if (Array.isArray(newValue)) {
changeMulti(newValue.map(v => (typeof v === "string" ? stringToOption(v) : v)));
} else {
changeMulti(
newValue
? [typeof newValue === "string" ? stringToOption(newValue) : newValue]
: null
);
}
} else {
console.warn("SearchSelect requires changeMulti to be defined when 'multiple' is true");
}
} else {
if (changeSelection) {
if (Array.isArray(newValue)) {
console.warn("changeSelection requires a single element but received an array");
} else {
changeSelection(typeof newValue === "string" ? stringToOption(newValue) : newValue);
}
} else {
console.warn(
"SearchSelect requires changeSelection to be defined when 'multiple' is false"
);
}
}
}
return (
<Autocomplete
id={label + "-autocomplete"}
value={selected === undefined ? null : selected}
multiple={multiple}
limitTags={2}
autoHighlight
disabled={disabled}
onChange={(_, newValue) => {
optionSelected(newValue)
}}
options={options.sort((a, b) => {
let aGroup = a.group ? a.group : "";
let bGroup = b.group ? b.group : "";
if (bGroup > aGroup) return -1;
if (aGroup > bGroup) return 1;
return b.label > a.label ? -1 : 1;
})}
filterOptions={(options, params) => {
const filtered = filter(options as Option[], params);
if (creatable && params.inputValue !== "") {
filtered.push({
value: params.inputValue,
label: params.inputValue,
new: true
});
}
return filtered;
}}
// getOptionLabel={option => {
// // return anyToLabel(option)
// // console.log(option)
// // console.log("Is Option: ",option instanceof Option)
// if (option instanceof Option) {
// return option.text + " - " + option.value
// // } else if (option.text && option.value) {
// } else {
// return anyToLabel(option)
// }
// }}
// getOptionSelected={
// getOptionSelected ? getOptionSelected : (o: { value: any; }, v: { value: any; }) => (o && v ? o.value === v.value : false)
// }
groupBy={group ? option => (option && option.group ? option.group : "") : undefined}
renderInput={params => (
<TextField
{...params}
label={label}
variant={variant ? variant : "outlined"}
onChange={onChange}
onKeyDown={onKeyDown}
InputProps={{
...params.InputProps,
startAdornment:
!isArray(selected) && selected?.icon ? <Avatar src={selected?.icon} /> : null,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
)
}}
/>
)}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
return (
<Box
key={key + ":" + option.value}
component="li"
sx={{ '& > img': { mr: 2, flexShrink: 0 } }}
{...optionProps}
>
<Box style={{ marginTop: "auto", marginBottom: "auto" }}>
{!isArray(option) && option?.icon ? <Avatar src={option?.icon} /> : ""}
</Box>
<Box style={{ marginTop: "auto", marginBottom: "auto", marginLeft: "1rem" }}>
{option.label}
</Box>
</Box>
)
}}
loading={loading}
selectOnFocus
clearOnBlur
freeSolo={creatable}
handleHomeEndKeys
fullWidth
classes={{
input: classes.input
}}
style={style}
/>
);
}