From f4d45abfca9f83239be653e90325ea17198090ed Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Mon, 2 Oct 2023 13:11:48 -0700 Subject: [PATCH] CEC-5150: search on fleets in bulk action (#454) * CEC-5150: search on fleets in bulk action * add deps * add deps * sonar * sonar * break out fetch --- .../BulkActions/actions/AddToFleet.jsx | 69 ++++++------------- src/components/BulkActions/index.jsx | 3 +- src/components/Cars/Update/index.jsx | 2 +- src/components/SearchSelect/SearchSelect.jsx | 64 +++++++++++++---- src/hooks/index.js | 1 + src/hooks/useTimeoutState.js | 21 ++++++ src/utils/truncateCSV.js | 13 ++++ src/utils/truncateCSV.test.js | 18 +++++ 8 files changed, 127 insertions(+), 64 deletions(-) create mode 100644 src/hooks/useTimeoutState.js create mode 100644 src/utils/truncateCSV.js create mode 100644 src/utils/truncateCSV.test.js diff --git a/src/components/BulkActions/actions/AddToFleet.jsx b/src/components/BulkActions/actions/AddToFleet.jsx index 39200da..e3adec5 100644 --- a/src/components/BulkActions/actions/AddToFleet.jsx +++ b/src/components/BulkActions/actions/AddToFleet.jsx @@ -1,23 +1,20 @@ -import { useEffect, useState, forwardRef, useImperativeHandle } from "react"; +import { useState, forwardRef, useImperativeHandle } from "react"; import { FormControl, - InputLabel, - MenuItem, - Select, } from '@material-ui/core'; +import SearchSelect from "../../SearchSelect/SearchSelect"; import { useStatusContext } from "../../Contexts/StatusContext"; import { useUserContext } from "../../Contexts/UserContext"; import fleetsAPI from "../../../services/fleetsAPI"; export default forwardRef(({ - vins, - vinCSV, + ids, + idCSV, }, ref) => { const { setMessage } = useStatusContext(); const { token: { idToken: { jwtToken: token } } } = useUserContext(); - const [fleet, setFleet] = useState(""); - const [options, setOptions] = useState([]); + const [fleet, setFleet] = useState(null); useImperativeHandle(ref, () => ({ async submit() { @@ -27,7 +24,7 @@ export default forwardRef(({ } return fleetsAPI - .addFleetVehicles(fleet, { vins }, token) + .addFleetVehicles(fleet, { vins: ids }, token) .then((response) => { if (response.error) { setMessage(`${response.error}: ${response.message}`); @@ -46,54 +43,32 @@ export default forwardRef(({ }, })); - useEffect(() => { - const controller = new AbortController(); - let isMounted = true; - - fleetsAPI + async function searchFleets(search) { + return fleetsAPI .getFleets({ - search: "", + search, limit: 10, offset: 0, order: `id desc`, - }, token, controller) - .then(({ data }) => { - if (isMounted) { - setOptions(data.map((fleet) => fleet.name)); - } - }); - return () => { - controller?.abort(); - isMounted = false; - } - }, [token]); - - const handleChange = (event) => { - setFleet(event.target.value); + }, token) + .then(response => response.data.map(fleet => fleet.name)) + .catch(() => []); } return (

- You are adding the following VINs to a fleet: {vinCSV}. + You are adding the following VINs to a fleet: {idCSV}.

- {options && ( - - - Fleet - - - - )} + + +
); }); \ No newline at end of file diff --git a/src/components/BulkActions/index.jsx b/src/components/BulkActions/index.jsx index 13a0de6..2b50dd8 100644 --- a/src/components/BulkActions/index.jsx +++ b/src/components/BulkActions/index.jsx @@ -3,6 +3,7 @@ import { hasRole, Permissions } from "../../utils/roles"; import DropDownButton from "../Controls/DropDownButton"; import { useUserContext } from "../Contexts/UserContext"; import { Modal } from "./Modal"; +import truncateCSV from "../../utils/truncateCSV"; // Code-splitting individual actions // https://react.dev/reference/react/lazy @@ -71,7 +72,7 @@ export default function BulkActions({ const payload = { ids, - idCSV: (ids && ids.length > 0) ? ids.join(", ") : "N/A", + idCSV: (ids && ids.length > 0) ? truncateCSV(ids, 10) : "N/A", ref: activeRef }; diff --git a/src/components/Cars/Update/index.jsx b/src/components/Cars/Update/index.jsx index 8f9f7a0..32aba3f 100644 --- a/src/components/Cars/Update/index.jsx +++ b/src/components/Cars/Update/index.jsx @@ -242,7 +242,7 @@ const MainForm = () => { label="SUMS Version" value={sumsVersion} setValue={setSumsVersion} - getData={async () => getSums()} + getData={getSums} /> { }, - getData = () => [], + getData = () => new Promise(), research = false, }) { const [open, setOpen] = React.useState(false); - const [searchCount, setSearchCount] = React.useState(0); + const [loading, setLoading] = React.useState(false); const [inputValue, setInputValue] = React.useState(""); - const [options, setOptions] = React.useState([{ label: value }]); + const [options, setOptions] = React.useState([]); + const [searchComplete, setSearchComplete] = React.useState(false); React.useEffect(() => { - function canSearch() { - if (research || searchCount === 0) { - return true; - } - return false; + if (!loading || searchComplete) { + return undefined; } - async function fetchData() { - setOptions(await getData(value)); + const debounce = setTimeout(async () => { + const data = await getData(inputValue); + setOptions(data); + if (!research) setSearchComplete(true); + }, research ? 500 : 0); // reduce queries while typing + + return () => { + clearTimeout(debounce); + }; + }, [research, loading, inputValue, getData, setOptions, searchComplete, setSearchComplete]); + + React.useEffect(() => { + if (!research || searchComplete) { + return undefined; } - if (!open && canSearch()) { - fetchData(); - setSearchCount((searchCount) => searchCount + 1); + setOptions([]); + }, [research, open, inputValue, setOptions, searchComplete]); + + React.useEffect(() => { + if (searchComplete) { + return undefined; } - }, [open, value, research, searchCount, getData]); + + setLoading(open && options.length === 0); + const timeout = setTimeout(() => { + setLoading(false); + }, 2000); // don't show loading forever + + return () => { + clearTimeout(timeout); + } + }, [searchComplete, open, options, setLoading]); return ( setOpen(true)} onClose={() => setOpen(false)} options={options} + loading={loading} + filterOptions={research ? (x) => x : createFilterOptions()} renderInput={(params) => + {loading ? : null} + {params.InputProps.endAdornment} + + ), + }} /> } /> diff --git a/src/hooks/index.js b/src/hooks/index.js index 36243f9..5fc34e4 100644 --- a/src/hooks/index.js +++ b/src/hooks/index.js @@ -1 +1,2 @@ +export { useTimeoutState } from "./useTimeoutState"; export { useUpdateManifest } from "./useUpdateManifest"; diff --git a/src/hooks/useTimeoutState.js b/src/hooks/useTimeoutState.js new file mode 100644 index 0000000..81bdaff --- /dev/null +++ b/src/hooks/useTimeoutState.js @@ -0,0 +1,21 @@ +import { useCallback, useState } from "react"; + +export const useTimeoutState = (defaultState) => { + const [state, _setState] = useState(defaultState); + const [currentTimeoutId, setCurrentTimeoutId] = useState(); + + const setState = useCallback( + (action, opts) => { + if (currentTimeoutId != null) { + clearTimeout(currentTimeoutId); + } + + _setState(action); + + const id = setTimeout(() => _setState(defaultState), opts?.timeout); + setCurrentTimeoutId(id); + }, + [currentTimeoutId, defaultState] + ); + return [state, setState]; +}; diff --git a/src/utils/truncateCSV.js b/src/utils/truncateCSV.js new file mode 100644 index 0000000..0d17d16 --- /dev/null +++ b/src/utils/truncateCSV.js @@ -0,0 +1,13 @@ +export default function truncateCSV(strings = [], max = Infinity) { + const count = strings.length; + + if (max === 0) { + return `${count}`; + } + + if (count <= max) { + return strings.join(", "); + } + + return `${strings.slice(0, max).join(", ")}, +${count - max} more`; +} \ No newline at end of file diff --git a/src/utils/truncateCSV.test.js b/src/utils/truncateCSV.test.js new file mode 100644 index 0000000..0e976b2 --- /dev/null +++ b/src/utils/truncateCSV.test.js @@ -0,0 +1,18 @@ +import truncateCSV from "./truncateCSV"; + +const tests = [ + [["ocean", "pear", "ronin", "alaska"], 4, "ocean, pear, ronin, alaska"], + [["ocean", "pear", "ronin", "alaska"], 3, "ocean, pear, ronin, +1 more"], + [["ocean", "pear", "ronin", "alaska"], 2, "ocean, pear, +2 more"], + [["ocean", "pear", "ronin", "alaska"], 0, "4"], + [["ocean", "pear", "ronin", "alaska"], 6, "ocean, pear, ronin, alaska"], + [["ocean", "pear", "ronin", "alaska"], Infinity, "ocean, pear, ronin, alaska"], +]; + +describe("truncateCSV", () => { + it("properly concatenates", () => { + tests.forEach(([strings, max, expected]) => { + expect(truncateCSV(strings, max)).toBe(expected); + }); + }); +});