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);
+ });
+ });
+});