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
This commit is contained in:
Tristan Timblin
2023-10-02 13:11:48 -07:00
committed by GitHub
parent 8d867a7a1e
commit f4d45abfca
8 changed files with 127 additions and 64 deletions

View File

@@ -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 (
<div>
<p>
You are adding the following VINs to a fleet: {vinCSV}.
You are adding the following VINs to a fleet: {idCSV}.
</p>
{options && (
<FormControl variant="filled" fullWidth={true}>
<InputLabel id="fleet-selection">
Fleet
</InputLabel>
<Select
labelId="fleet-selection"
value={fleet}
label="Fleet"
onChange={handleChange}
>
{options.map((option) => (
<MenuItem key={option} value={option}>{option}</MenuItem>
))}
</Select>
</FormControl>
)}
<FormControl variant="filled" fullWidth={true}>
<SearchSelect
label="Fleet"
value={fleet}
setValue={setFleet}
getData={searchFleets}
research={true}
/>
</FormControl>
</div>
);
});

View File

@@ -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
};

View File

@@ -242,7 +242,7 @@ const MainForm = () => {
label="SUMS Version"
value={sumsVersion}
setValue={setSumsVersion}
getData={async () => getSums()}
getData={getSums}
/>
<TextField
id="model"

View File

@@ -1,36 +1,59 @@
import * as React from 'react';
import TextField from '@mui/material/TextField';
import Autocomplete from '@mui/material/Autocomplete';
import Autocomplete, { createFilterOptions } from '@mui/material/Autocomplete';
import CircularProgress from '@mui/material/CircularProgress';
export default function SearchSelect({
label = "",
value = "",
setValue = () => { },
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 (
<Autocomplete
@@ -43,6 +66,8 @@ export default function SearchSelect({
onOpen={() => setOpen(true)}
onClose={() => setOpen(false)}
options={options}
loading={loading}
filterOptions={research ? (x) => x : createFilterOptions()}
renderInput={(params) =>
<TextField
{...params}
@@ -50,6 +75,15 @@ export default function SearchSelect({
name={label}
variant="outlined"
margin="normal"
InputProps={{
...params.InputProps,
endAdornment: (
<React.Fragment>
{loading ? <CircularProgress color="inherit" size={20} /> : null}
{params.InputProps.endAdornment}
</React.Fragment>
),
}}
/>
}
/>

View File

@@ -1 +1,2 @@
export { useTimeoutState } from "./useTimeoutState";
export { useUpdateManifest } from "./useUpdateManifest";

View File

@@ -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];
};

13
src/utils/truncateCSV.js Normal file
View File

@@ -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`;
}

View File

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