diff --git a/client/src/Components/Inputs/Search/index.jsx b/client/src/Components/Inputs/Search/index.jsx index e5654faeb..4624b2cba 100644 --- a/client/src/Components/Inputs/Search/index.jsx +++ b/client/src/Components/Inputs/Search/index.jsx @@ -1,7 +1,17 @@ import PropTypes from "prop-types"; -import { Box, ListItem, Autocomplete, TextField, Stack, Typography } from "@mui/material"; +import { + Box, + ListItem, + Autocomplete, + TextField, + Stack, + 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"; /** * Search component using Material UI's Autocomplete. @@ -60,24 +70,72 @@ const Search = ({ onBlur, }) => { 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 ( setOpen(true)} + onClose={(event, reason) => { + if (reason === "blur" || reason === "escape") { + setOpen(false); + } + }} inputValue={inputValue} onInputChange={(_, newValue) => { handleInputChange(newValue); }} onChange={(_, newValue) => { - handleChange(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={options} + options={enhancedOptions} getOptionLabel={(option) => option[filteredBy]} isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier renderInput={(params) => ( @@ -120,6 +178,9 @@ const Search = ({ )} filterOptions={(options, { inputValue }) => { + if (inputValue.trim() === "" && multiple && isAdorned) { + return enhancedOptions; + } const filtered = options.filter((option) => option[filteredBy].toLowerCase().includes(inputValue.toLowerCase()) ); @@ -136,6 +197,7 @@ const Search = ({ const { key, ...optionProps } = props; const hasSecondaryLabel = secondaryLabel && option[secondaryLabel] !== undefined; const port = option["port"]; + const selected = isOptionSelected(option); return ( + {multiple && isAdorned && !option.noOptions && ( + + )} {option[filteredBy] + (hasSecondaryLabel ? ` (${option[secondaryLabel]}${port ? `: ${port}` : ""})` diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 7d2cdbfa7..ff878c3da 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -744,5 +744,6 @@ "settingsEmailRejectUnauthorized": "Reject Unauthorized", "settingsEmailSecure": "Secure - Use SSL", "settingsEmailPool": "Pool - Enable connection pooling", - "sendTestNotifications": "Send test notifications" -} + "sendTestNotifications": "Send test notifications", + "selectAll": "Select all" +} \ No newline at end of file