diff --git a/client/src/components/Projects/ColumnHeaderPopups/MultiSelectText.js b/client/src/components/Projects/ColumnHeaderPopups/MultiSelectText.js index 1fe442b5..beb13f0d 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/MultiSelectText.js +++ b/client/src/components/Projects/ColumnHeaderPopups/MultiSelectText.js @@ -34,6 +34,9 @@ const useStyles = createUseStyles({ const MultiSelectText = ({ options, selectedOptions, setSelectedOptions }) => { const classes = useStyles(); const [searchString, setSearchString] = useState(""); + const [selectedListItems, setSelectedListItems] = useState([ + ...selectedOptions + ]); const filteredOptions = options .filter(o => !!o) @@ -43,17 +46,31 @@ const MultiSelectText = ({ options, selectedOptions, setSelectedOptions }) => { setSearchString(e.target.value); }; - const handleCheckboxChange = o => { - if (selectedOptions.find(so => so.value == o)) { - setSelectedOptions( - selectedOptions.filter(selectedOption => selectedOption.value !== o) + const handleCheckboxChange = e => { + const optionValue = e.target.name; + if (selectedListItems.find(so => so.value == optionValue)) { + const newSelectedListItems = selectedListItems.filter( + selectedOption => selectedOption.value !== optionValue ); + setSelectedListItems(newSelectedListItems); + setSelectedOptions(newSelectedListItems); } else { - selectedOptions.push({ value: o, label: o }); - setSelectedOptions(selectedOptions); + const newSelectedListItems = [ + ...selectedListItems, + { value: optionValue, label: optionValue } + ]; + setSelectedListItems(newSelectedListItems); + setSelectedOptions(newSelectedListItems); } }; + const isChecked = optionValue => { + const checked = selectedListItems.find( + option => option.value == optionValue + ); + return !!checked; + }; + return ( <> <div className={classes.searchBarWrapper}> @@ -72,16 +89,17 @@ const MultiSelectText = ({ options, selectedOptions, setSelectedOptions }) => { </div> <div style={{ overflow: "scroll", maxHeight: "15rem" }}> - {/* <pre>{JSON.stringify(selectedOptions, null, 2)}</pre> - <pre>{JSON.stringify(options, null, 2)}</pre> */} + <pre>{JSON.stringify(selectedOptions, null, 2)}</pre> + {/* <pre>{JSON.stringify(options, null, 2)}</pre> */} {filteredOptions.map(o => ( <div key={o} className={classes.listItem}> <input style={{ height: "1.5rem" }} type="checkbox" - checked={selectedOptions.find(option => option.value == o)} - onChange={() => handleCheckboxChange(o)} + name={o} + checked={isChecked(o)} + onChange={handleCheckboxChange} /> <span>{o}</span> </div> diff --git a/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.js b/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.js index 9059b0c5..a5667978 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.js +++ b/client/src/components/Projects/ColumnHeaderPopups/ProjectTableColumnHeader.js @@ -1,12 +1,13 @@ import React from "react"; import PropTypes from "prop-types"; import "react-datepicker/dist/react-datepicker.css"; -import { MdFilterAlt } from "react-icons/md"; +import { MdFilterAlt, MdFilterList } from "react-icons/md"; import Popup from "reactjs-popup"; import DatePopup from "./DatePopup"; import TextPopup from "./TextPopup"; import VisibilityPopup from "./VisibilityPopup"; import StatusPopup from "./StatusPopup"; +import { useTheme } from "react-jss"; const ProjectTableColumnHeader = ({ projects, @@ -20,6 +21,31 @@ const ProjectTableColumnHeader = ({ setCheckedProjectIds, setSelectAllChecked }) => { + const theme = useTheme(); + + // Filter is considered Applied if it is not set + // to the default criteria values. + const isFilterApplied = () => { + let propertyName = header.id; + if (header.popupType === "text") { + propertyName += "List"; + return criteria[propertyName].length > 0; + } + if (header.popupType === "datetime") { + return ( + criteria[header.startDatePropertyName] || + criteria[header.endDatePropertyName] + ); + } + if (header.id === "dateHidden") { + return criteria.visibility !== "visible"; + } + if (header.id === "dateSnapshotted") { + return criteria.type !== "all" || criteria.status !== "active"; + } + return false; + }; + return ( <div style={{ width: "100%", height: "100%" }}> {header.id !== "checkAllProjects" && header.id !== "contextMenu" ? ( @@ -33,14 +59,25 @@ const ProjectTableColumnHeader = ({ }} > <span>{header.label}</span> - <MdFilterAlt - style={{ - backgroundColor: "transparent", - color: "white", - marginLeft: "0.5rem" - }} - alt={`Show column filter and sort popup`} - /> + {isFilterApplied() ? ( + <MdFilterAlt + style={{ + backgroundColor: "transparent", + color: "white", + marginLeft: "0.5rem" + }} + alt={`Show column filter and sort popup`} + /> + ) : ( + <MdFilterList + style={{ + backgroundColor: "transparent", + color: theme.colorLightGray, + marginLeft: "0.5rem" + }} + alt={`Show column filter and sort popup`} + /> + )} {/* <FontAwesomeIcon style={{ backgroundColor: "transparent", diff --git a/client/src/components/Projects/ColumnHeaderPopups/StatusPopup.js b/client/src/components/Projects/ColumnHeaderPopups/StatusPopup.js index fffea250..5a1021e7 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/StatusPopup.js +++ b/client/src/components/Projects/ColumnHeaderPopups/StatusPopup.js @@ -26,6 +26,8 @@ const StatusPopup = ({ // TODO More state variables for status filtering go here const setDefault = () => { + setTypeSetting("all"); + setShowDeleted(false); setCriteria({ ...criteria, status: showDeleted ? "all" : "active", diff --git a/client/src/components/Projects/ColumnHeaderPopups/TextPopup.js b/client/src/components/Projects/ColumnHeaderPopups/TextPopup.js index 5d6a0a01..3f5c9cda 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/TextPopup.js +++ b/client/src/components/Projects/ColumnHeaderPopups/TextPopup.js @@ -6,7 +6,46 @@ import "react-datepicker/dist/react-datepicker.css"; import { MdClose } from "react-icons/md"; import SearchIcon from "../../../images/search.png"; import Select from "react-select"; -import MultiSelectText from "./MultiSelectText"; +import { createUseStyles } from "react-jss"; + +const useStyles = createUseStyles({ + searchBarWrapper: { + position: "relative", + alignSelf: "center", + marginBottom: "0.5rem" + }, + searchBar: { + maxWidth: "100%", + width: "20em", + padding: "12px 12px 12px 48px", + marginRight: "0.5rem" + }, + searchIcon: { + position: "absolute", + left: "16px", + top: "14px" + }, + listItem: { + display: "flex", + flexDirection: "row", + alignItems: "center", + height: "2rem", + "&:hover": { + backgroundColor: "lightblue" + } + }, + toggleButton: { + marginRight: "0", + marginTop: "4px", + marginBottom: "4px", + backgroundColor: "transparent", + border: "0", + cursor: "pointer", + textDecoration: "underline", + display: "flex", + fontWeight: "normal" + } +}); const TextPopup = ({ projects, @@ -21,12 +60,15 @@ const TextPopup = ({ setCheckedProjectIds, setSelectAllChecked }) => { + const classes = useStyles(); + const [newOrder, setNewOrder] = useState( header.id !== orderBy ? null : order ); const [selectedListItems, setSelectedListItems] = useState( criteria[header.id + "List"].map(s => ({ value: s, label: s })) ); + const [searchString, setSearchString] = useState(""); // To build the drop-down list, we want to apply all the criteria that // are currently selected EXCEPT the criteria we are currently editing. @@ -37,6 +79,37 @@ const TextPopup = ({ .filter(value => value !== null) .sort(); + const filteredOptions = selectOptions + .filter(o => !!o) + .filter(opt => opt.toLowerCase().includes(searchString.toLowerCase())); + + const onChangeSearchString = e => { + setSearchString(e.target.value); + }; + + const handleCheckboxChange = e => { + const optionValue = e.target.name; + if (!e.target.checked) { + const newSelectedListItems = selectedListItems.filter( + selectedOption => selectedOption.value !== optionValue + ); + setSelectedListItems(newSelectedListItems); + } else { + const newSelectedListItems = [ + ...selectedListItems, + { value: optionValue, label: optionValue } + ]; + setSelectedListItems(newSelectedListItems); + } + }; + + const isChecked = optionValue => { + const checked = selectedListItems.find( + option => option.value == optionValue + ); + return !!checked; + }; + const applyChanges = () => { setCriteria({ ...criteria, @@ -51,6 +124,7 @@ const TextPopup = ({ }; const setDefault = () => { + setNewOrder(null); setSelectedListItems([]); setCheckedProjectIds([]); setSelectAllChecked(false); @@ -101,13 +175,60 @@ const TextPopup = ({ so PMs, designers and possibly stakeholders can evaluate two different implementations of the TextPopup, and experiment with the UX, in order to make a decision on how to evolve this filter. */} {header.id === "address" ? ( - <MultiSelectText - options={selectOptions} - selectedOptions={selectedListItems} - setSelectedOptions={e => { - setSelectedListItems(e); - }} - /> + // <MultiSelectText + // options={selectOptions} + // selectedOptions={selectedListItems} + // setSelectedOptions={setSelectedListItems} + // /> + <> + <div + style={{ + display: "flex", + justifyContent: "space-between", + alignItems: "baseline" + }} + > + <button + className={classes.toggleButton} + onClick={() => setSelectedListItems([])} + > + clear + </button> + <div>{`${selectedListItems.length} selected`}</div> + </div> + <div className={classes.searchBarWrapper}> + <input + type="text" + value={searchString} + onChange={onChangeSearchString} + placeholder="Search" + className={classes.searchBar} + /> + <img + className={classes.searchIcon} + src={SearchIcon} + alt="Search Icon" + /> + </div> + + <div style={{ overflow: "scroll", maxHeight: "15rem" }}> + {/* <pre>{JSON.stringify(selectedListItems, null, 2)}</pre> */} + {/* <pre>{JSON.stringify(options, null, 2)}</pre> */} + + {filteredOptions.map(o => ( + <div key={o} className={classes.listItem}> + <input + style={{ height: "1.5rem" }} + type="checkbox" + name={o} + checked={isChecked(o)} + onChange={handleCheckboxChange} + /> + <span>{o}</span> + </div> + ))} + </div> + </> ) : ( <Select options={selectOptions.map(text => ({ @@ -116,9 +237,7 @@ const TextPopup = ({ }))} name={property} disabled={false} - onChange={e => { - setSelectedListItems(e); - }} + onChange={setSelectedListItems} value={selectedListItems} styles={{ maxHeight: "50rem", maxWidth: "50rem" }} placeholder={placeholderComponent} diff --git a/client/src/components/Projects/ColumnHeaderPopups/VisibilityPopup.js b/client/src/components/Projects/ColumnHeaderPopups/VisibilityPopup.js index 57822335..95aac0bb 100644 --- a/client/src/components/Projects/ColumnHeaderPopups/VisibilityPopup.js +++ b/client/src/components/Projects/ColumnHeaderPopups/VisibilityPopup.js @@ -25,6 +25,7 @@ const VisibilityPopup = ({ ); const setDefault = () => { + setVisibilitySetting("visible"); setCriteria({ ...criteria, visibility: "visible" diff --git a/client/src/styles/theme.js b/client/src/styles/theme.js index 0b0cb130..b3f9409b 100644 --- a/client/src/styles/theme.js +++ b/client/src/styles/theme.js @@ -5,6 +5,8 @@ module.exports = { colorPrimary: "#a7c539", //lime green colorText: "#0F2940", //dark blue colorLADOT: "#002E6D", + colorGray: "#808080", + colorLightGray: "#A0A0A0", colorEarnedPoints: "rgb(255, 168, 4)", //orange colorDisabled: "rgba(0, 0, 0, .05)", //lightest grey transparent colorCancel: "rgba(0, 0, 0, 0.5)", //light grey, e.g. cancel button