316 lines
8.2 KiB
React
316 lines
8.2 KiB
React
import PropTypes from "prop-types";
|
|
import {
|
|
Box,
|
|
ListItem,
|
|
Autocomplete,
|
|
TextField,
|
|
Typography,
|
|
Checkbox,
|
|
} from "@mui/material";
|
|
import { useTheme } from "@emotion/react";
|
|
import SearchIcon from "../../../assets/icons/search.svg?react";
|
|
import React, { useEffect } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import FieldWrapper from "../FieldWrapper";
|
|
|
|
/**
|
|
* Search component using Material UI's Autocomplete.
|
|
*
|
|
* @param {Object} props
|
|
* @param {string} props.id - Unique identifier for the Autocomplete component
|
|
* @param {Array<Object>} props.options - Options to display in the Autocomplete dropdown
|
|
* @param {string} props.filteredBy - Key to access the option label from the options
|
|
* @param {string} props.value - Current input value for the Autocomplete
|
|
* @param {Function} props.handleChange - Function to call when the input changes
|
|
* @param {Function} Prop.onBlur - Function to call when the input is blured
|
|
* @param {Object} props.sx - Additional styles to apply to the component
|
|
* @param {string} props.unit - Label to identify type of options
|
|
* @returns {JSX.Element} The rendered Search component
|
|
*/
|
|
|
|
const SearchAdornment = () => {
|
|
const theme = useTheme();
|
|
return (
|
|
<Box
|
|
height={16}
|
|
sx={{
|
|
"& svg": {
|
|
width: 16,
|
|
height: 16,
|
|
"& path": {
|
|
stroke: theme.palette.primary.contrastTextTertiary,
|
|
strokeWidth: 1.2,
|
|
},
|
|
},
|
|
}}
|
|
>
|
|
<SearchIcon />
|
|
</Box>
|
|
);
|
|
};
|
|
|
|
//TODO keep search state inside of component.
|
|
const Search = ({
|
|
label,
|
|
id,
|
|
options,
|
|
filteredBy,
|
|
secondaryLabel,
|
|
value,
|
|
inputValue,
|
|
handleInputChange,
|
|
handleChange,
|
|
sx,
|
|
multiple = false,
|
|
isAdorned = true,
|
|
error,
|
|
disabled,
|
|
startAdornment,
|
|
endAdornment,
|
|
onBlur,
|
|
//FieldWrapper's props
|
|
gap,
|
|
labelMb,
|
|
labelFontWeight,
|
|
labelVariant,
|
|
labelSx = {},
|
|
unit = "option",
|
|
maxWidth = "100%",
|
|
}) => {
|
|
const theme = useTheme();
|
|
const { t } = useTranslation();
|
|
const [selectAll, setSelectAll] = React.useState(false);
|
|
|
|
const [open, setOpen] = React.useState(false);
|
|
const enhancedOptions = React.useMemo(() => {
|
|
return multiple && isAdorned
|
|
? [
|
|
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
|
|
...options,
|
|
]
|
|
: options;
|
|
}, [multiple, isAdorned, options, filteredBy]);
|
|
const isOptionSelected = (option) => {
|
|
if (!multiple && !isAdorned) return false;
|
|
if (Array.isArray(value)) {
|
|
return value.some((item) => item._id === option._id);
|
|
}
|
|
return false;
|
|
};
|
|
const handleSelectAll = (isSelectAll) => {
|
|
const newValue = isSelectAll ? [...options] : [];
|
|
handleChange(newValue);
|
|
setSelectAll(isSelectAll);
|
|
};
|
|
useEffect(() => {
|
|
const allSelected =
|
|
Array.isArray(value) && Array.isArray(options) && value.length === options.length;
|
|
if (selectAll !== allSelected) setSelectAll(allSelected);
|
|
}, [value, options]);
|
|
return (
|
|
<Autocomplete
|
|
onBlur={onBlur}
|
|
multiple={multiple}
|
|
id={id}
|
|
value={value}
|
|
open={open}
|
|
onOpen={() => setOpen(true)}
|
|
onClose={(event, reason) => {
|
|
if (reason === "blur" || reason === "escape") {
|
|
setOpen(false);
|
|
}
|
|
}}
|
|
inputValue={inputValue}
|
|
onInputChange={(_, newValue) => {
|
|
handleInputChange(newValue);
|
|
}}
|
|
onChange={(_, newValue) => {
|
|
if (multiple && isAdorned) {
|
|
const hasSelectAllSelected =
|
|
Array.isArray(newValue) && newValue.some((item) => item.isSelectAll);
|
|
if (hasSelectAllSelected) {
|
|
handleSelectAll(!selectAll);
|
|
} else {
|
|
handleChange(newValue);
|
|
setSelectAll(Array.isArray(newValue) && newValue.length === options.length);
|
|
}
|
|
} else {
|
|
handleChange(newValue);
|
|
setOpen(false);
|
|
}
|
|
}}
|
|
fullWidth
|
|
freeSolo
|
|
disabled={disabled}
|
|
disableClearable
|
|
options={enhancedOptions}
|
|
getOptionLabel={(option) => option[filteredBy]}
|
|
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
|
|
renderInput={(params) => (
|
|
<FieldWrapper
|
|
label={label}
|
|
labelMb={labelMb}
|
|
labelVariant={labelVariant}
|
|
labelFontWeight={labelFontWeight}
|
|
labelSx={labelSx}
|
|
gap={gap}
|
|
sx={{
|
|
...sx,
|
|
}}
|
|
>
|
|
<TextField
|
|
{...params}
|
|
error={Boolean(error)}
|
|
placeholder="Type to search"
|
|
slotProps={{
|
|
input: {
|
|
...params.InputProps,
|
|
...(isAdorned && { startAdornment: <SearchAdornment /> }),
|
|
...(startAdornment && { startAdornment: startAdornment }),
|
|
...(endAdornment && { endAdornment: endAdornment }),
|
|
},
|
|
}}
|
|
sx={{}}
|
|
/>
|
|
{error && (
|
|
<Typography
|
|
component="span"
|
|
className="input-error"
|
|
color={theme.palette.error.main}
|
|
mt={theme.spacing(2)}
|
|
sx={{
|
|
opacity: 0.8,
|
|
}}
|
|
>
|
|
{error}
|
|
</Typography>
|
|
)}
|
|
</FieldWrapper>
|
|
)}
|
|
filterOptions={(options, { inputValue }) => {
|
|
if (inputValue.trim() === "" && multiple && isAdorned) {
|
|
return enhancedOptions;
|
|
}
|
|
const filtered = options.filter((option) =>
|
|
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
|
|
);
|
|
|
|
if (filtered.length === 0) {
|
|
return [
|
|
{
|
|
[filteredBy]: t("general.noOptionsFound", { unit: unit }),
|
|
noOptions: true,
|
|
},
|
|
];
|
|
}
|
|
return filtered;
|
|
}}
|
|
getOptionKey={(option) => {
|
|
return option._id;
|
|
}}
|
|
renderOption={(props, option) => {
|
|
const { key, ...optionProps } = props;
|
|
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
|
|
const port = option["port"];
|
|
const selected = isOptionSelected(option);
|
|
return (
|
|
<ListItem
|
|
key={key}
|
|
{...optionProps}
|
|
sx={
|
|
option.noOptions
|
|
? {
|
|
pointerEvents: "none",
|
|
backgroundColor: theme.palette.primary.main,
|
|
}
|
|
: option.isSelectAll
|
|
? {
|
|
fontWeight: "bold",
|
|
backgroundColor: theme.palette.primary.light,
|
|
"&:hover": {
|
|
backgroundColor: theme.palette.primary.light,
|
|
},
|
|
}
|
|
: {}
|
|
}
|
|
>
|
|
{multiple && isAdorned && !option.noOptions && (
|
|
<Checkbox
|
|
checked={option.isSelectAll ? selectAll : selected}
|
|
sx={{
|
|
color: theme.palette.primary.contrastTextSecondary,
|
|
"&.Mui-checked": {
|
|
color: theme.palette.secondary.main,
|
|
},
|
|
padding: 0,
|
|
}}
|
|
/>
|
|
)}
|
|
{option[filteredBy] +
|
|
(hasSecondaryLabel
|
|
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
|
|
: "")}
|
|
</ListItem>
|
|
);
|
|
}}
|
|
slotProps={{
|
|
popper: {
|
|
keepMounted: true,
|
|
sx: {
|
|
"& ul": { p: 2, backgroundColor: theme.palette.primary.main },
|
|
"& li.MuiAutocomplete-option": {
|
|
color: theme.palette.primary.contrastTextSecondary,
|
|
px: 4,
|
|
borderRadius: theme.shape.borderRadius,
|
|
},
|
|
|
|
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
|
|
{
|
|
backgroundColor: theme.palette.primary.lowContrast,
|
|
color: "red",
|
|
},
|
|
"& li.MuiAutocomplete-option:hover:not([aria-selected='true'])": {
|
|
color: theme.palette.secondary.contrastText,
|
|
backgroundColor: theme.palette.secondary.main,
|
|
},
|
|
"& .MuiAutocomplete-noOptions": {
|
|
px: theme.spacing(6),
|
|
py: theme.spacing(5),
|
|
},
|
|
},
|
|
},
|
|
}}
|
|
sx={{
|
|
/* height: 34,*/
|
|
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
|
|
padding: `0 ${theme.spacing(5)}`,
|
|
},
|
|
...sx,
|
|
}}
|
|
/>
|
|
);
|
|
};
|
|
|
|
Search.propTypes = {
|
|
label: PropTypes.string,
|
|
id: PropTypes.string,
|
|
multiple: PropTypes.bool,
|
|
options: PropTypes.array.isRequired,
|
|
filteredBy: PropTypes.string.isRequired,
|
|
secondaryLabel: PropTypes.string,
|
|
value: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
|
|
inputValue: PropTypes.string.isRequired,
|
|
handleInputChange: PropTypes.func.isRequired,
|
|
handleChange: PropTypes.func,
|
|
isAdorned: PropTypes.bool,
|
|
sx: PropTypes.object,
|
|
error: PropTypes.string,
|
|
disabled: PropTypes.bool,
|
|
startAdornment: PropTypes.object,
|
|
endAdornment: PropTypes.object,
|
|
onBlur: PropTypes.func,
|
|
unit: PropTypes.string,
|
|
};
|
|
|
|
export default Search;
|