Files
Checkmate/client/src/Components/Inputs/Search/index.jsx
T

297 lines
7.8 KiB
React
Raw Normal View History

2024-09-18 17:14:03 -04:00
import PropTypes from "prop-types";
2025-06-17 10:13:58 +05:30
import {
Box,
ListItem,
Autocomplete,
TextField,
Stack,
Typography,
Checkbox,
} from "@mui/material";
2024-09-18 17:14:03 -04:00
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../assets/icons/search.svg?react";
2025-06-17 10:13:58 +05:30
import React, { useEffect } from "react";
2024-09-18 17:14:03 -04:00
2024-09-18 17:24:36 -04:00
/**
* 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
2024-09-18 20:27:55 -04:00
* @param {string} props.filteredBy - Key to access the option label from the options
2024-09-18 17:24:36 -04:00
* @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
2024-09-18 17:24:36 -04:00
* @param {Object} props.sx - Additional styles to apply to the component
* @returns {JSX.Element} The rendered Search component
*/
2024-10-03 15:24:22 +08:00
const SearchAdornment = () => {
const theme = useTheme();
return (
<Box
mr={theme.spacing(4)}
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
2025-01-12 09:39:31 -05:00
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
</Box>
);
2024-10-03 15:24:22 +08:00
};
//TODO keep search state inside of component
2024-09-19 11:07:38 +08:00
const Search = ({
2024-12-18 12:20:18 -08:00
label,
id,
options,
filteredBy,
secondaryLabel,
value,
inputValue,
handleInputChange,
handleChange,
sx,
multiple = false,
isAdorned = true,
error,
disabled,
2024-12-27 20:59:03 -05:00
startAdornment,
endAdornment,
2025-02-01 18:36:43 -08:00
onBlur,
2024-09-19 11:07:38 +08:00
}) => {
const theme = useTheme();
2025-06-17 10:13:58 +05:30
const [selectAll, setSelectAll] = React.useState(false);
useEffect(() => {
setSelectAll(
multiple &&
isAdorned &&
Array.isArray(value) &&
Array.isArray(options) &&
options.length > 0 &&
value.length === options.length
);
}, [multiple, isAdorned, options, value]);
const [open, setOpen] = React.useState(false);
const enhancedOptions = React.useMemo(() => {
return multiple && isAdorned
? [{ [filteredBy]: "Select All", 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);
setOpen(!isSelectAll);
};
return (
<Autocomplete
onBlur={onBlur}
multiple={multiple}
id={id}
value={value}
2025-06-17 10:13:58 +05:30
open={open}
onOpen={() => setOpen(true)}
onClose={(event, reason) => {
if (reason === "blur" || reason === "escape") {
setOpen(false);
}
}}
inputValue={inputValue}
onInputChange={(_, newValue) => {
handleInputChange(newValue);
}}
2025-02-01 18:36:43 -08:00
onChange={(_, newValue) => {
2025-06-17 10:13:58 +05:30
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={options}
2025-02-01 18:36:43 -08:00
getOptionLabel={(option) => option[filteredBy]}
2025-02-05 10:56:50 -08:00
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
renderInput={(params) => (
<Stack>
2024-12-18 12:20:18 -08:00
<Typography
component="h3"
fontSize={"var(--env-var-font-size-medium)"}
2025-01-12 09:39:31 -05:00
color={theme.palette.primary.contrastTextSecondary}
2024-12-18 12:20:18 -08:00
fontWeight={500}
>
{label}
</Typography>
<TextField
{...params}
error={Boolean(error)}
placeholder="Type to search"
2024-12-27 20:59:03 -05:00
slotProps={{
input: {
...params.InputProps,
...(isAdorned && { startAdornment: <SearchAdornment /> }),
...(startAdornment && { startAdornment: startAdornment }),
...(endAdornment && { endAdornment: endAdornment }),
},
}}
2025-06-13 13:49:54 +08:00
sx={{}}
/>
{error && (
<Typography
component="span"
className="input-error"
2025-03-11 23:30:41 +05:30
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{error}
</Typography>
)}
</Stack>
)}
filterOptions={(options, { inputValue }) => {
2025-06-17 10:13:58 +05:30
if (inputValue.trim() === "" && multiple && isAdorned) {
return enhancedOptions;
}
const filtered = options.filter((option) =>
option[filteredBy].toLowerCase().includes(inputValue.toLowerCase())
);
2024-09-18 17:14:03 -04:00
if (filtered.length === 0) {
return [{ [filteredBy]: "No monitors found", noOptions: true }];
}
return filtered;
}}
2025-01-28 15:37:49 -08:00
getOptionKey={(option) => {
return option._id;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined;
2025-05-25 15:54:31 +05:30
const port = option["port"];
2025-06-17 10:13:58 +05:30
const selected = isOptionSelected(option);
return (
<ListItem
key={key}
{...optionProps}
sx={
option.noOptions
? {
pointerEvents: "none",
backgroundColor: theme.palette.primary.main,
}
2025-06-17 10:13:58 +05:30
: option.isSelectAll
? {
fontWeight: "bold",
backgroundColor: theme.palette.primary.light,
"&:hover": {
backgroundColor: theme.palette.primary.light,
},
}
: {}
}
>
2025-06-17 10:13:58 +05:30
{multiple && isAdorned && (
<Checkbox
checked={option.isSelectAll ? selectAll : selected}
sx={{
color: theme.palette.primary.contrastTextSecondary,
"&.Mui-checked": {
color: theme.palette.secondary.main,
},
padding: 0,
}}
/>
)}
{option[filteredBy] +
2025-05-25 15:54:31 +05:30
(hasSecondaryLabel
2025-05-26 21:13:05 +05:30
? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})`
2025-05-25 15:54:31 +05:30
: "")}
</ListItem>
);
}}
slotProps={{
popper: {
keepMounted: true,
sx: {
2025-01-09 12:32:54 -05:00
"& ul": { p: 2, backgroundColor: theme.palette.primary.main },
"& li.MuiAutocomplete-option": {
2025-01-12 09:39:31 -05:00
color: theme.palette.primary.contrastTextSecondary,
px: 4,
borderRadius: theme.shape.borderRadius,
},
2025-01-09 12:32:54 -05:00
"& .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'], & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true'].Mui-focused, & .MuiAutocomplete-listbox .MuiAutocomplete-option[aria-selected='true']:hover":
{
2025-01-08 21:41:59 -05:00
backgroundColor: theme.palette.primary.lowContrast,
2025-01-22 12:40:29 -08:00
color: "red",
},
2025-01-09 12:32:54 -05:00
"& li.MuiAutocomplete-option:hover:not([aria-selected='true'])": {
color: theme.palette.secondary.contrastText,
backgroundColor: theme.palette.secondary.main,
2025-01-09 12:32:54 -05:00
},
"& .MuiAutocomplete-noOptions": {
px: theme.spacing(6),
py: theme.spacing(5),
},
},
},
}}
sx={{
2024-11-29 14:06:59 -05:00
/* height: 34,*/
"&.MuiAutocomplete-root .MuiAutocomplete-input": { p: 0 },
...sx,
}}
/>
);
2024-09-18 17:14:03 -04:00
};
2024-09-18 17:24:36 -04:00
Search.propTypes = {
2025-01-28 15:37:49 -08:00
label: PropTypes.string,
id: PropTypes.string,
multiple: PropTypes.bool,
options: PropTypes.array.isRequired,
filteredBy: PropTypes.string.isRequired,
secondaryLabel: PropTypes.string,
2025-02-01 18:36:43 -08:00
value: PropTypes.array,
inputValue: PropTypes.string.isRequired,
handleInputChange: PropTypes.func.isRequired,
handleChange: PropTypes.func,
isAdorned: PropTypes.bool,
sx: PropTypes.object,
error: PropTypes.string,
disabled: PropTypes.bool,
2024-12-27 20:59:03 -05:00
startAdornment: PropTypes.object,
endAdornment: PropTypes.object,
2025-02-01 18:36:43 -08:00
onBlur: PropTypes.func,
2024-09-18 17:24:36 -04:00
};
2024-09-18 17:14:03 -04:00
export default Search;