CEC-5085: add search on VINs support (#450)

This commit is contained in:
Tristan Timblin
2023-09-22 10:53:58 -07:00
committed by GitHub
parent 3177d65e3d
commit 842c402d05
3 changed files with 139 additions and 4 deletions

View File

@@ -13,15 +13,15 @@ import OptionsDropdown from "../../Controls/OptionsDropdown";
import { RoleWrap } from "../../Controls/RoleWrap"; import { RoleWrap } from "../../Controls/RoleWrap";
import SearchField from "../../Controls/SearchField"; import SearchField from "../../Controls/SearchField";
import BulkActions from "../../BulkActions"; import BulkActions from "../../BulkActions";
import { useLocalStorage } from "../../useLocalStorage"; import useQuery from "./useQuery";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
const MainForm = () => { const MainForm = () => {
const classes = useStyles(); const classes = useStyles();
const [search, setSearch] = useLocalStorage("VEHICLE_SEARCH", "");
const [online, setOnline] = useState(false); const [online, setOnline] = useState(false);
const [onlineHMI, setOnlineHMI] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false);
const [selectedVins, setSelectedVins] = useState([]); const [selectedVins, setSelectedVins] = useState([]);
const { vins, search, query, setQuery } = useQuery();
const { setTitle, setSitePath } = useStatusContext(); const { setTitle, setSitePath } = useStatusContext();
const { const {
token: { token: {
@@ -32,7 +32,7 @@ const MainForm = () => {
} = useUserContext(); } = useUserContext();
const handleSearch = (query) => { const handleSearch = (query) => {
setSearch(query); setQuery(query);
}; };
const handleOnline = (event) => { const handleOnline = (event) => {
@@ -78,7 +78,7 @@ const MainForm = () => {
<BulkActions vins={selectedVins} actions={["addTags", "addToFleet", "deleteVehicles", "updateConfig"]} /> <BulkActions vins={selectedVins} actions={["addTags", "addToFleet", "deleteVehicles", "updateConfig"]} />
</Grid> </Grid>
<Grid item md={4} className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} /> <SearchField classes={classes} onSearch={handleSearch} savedSearchValue={query} />
</Grid> </Grid>
<Grid item md={2} className={clsx(classes.textJustifyAlign, classes.actionsBar)}> <Grid item md={2} className={clsx(classes.textJustifyAlign, classes.actionsBar)}>
<OptionsDropdown listId="filter-menu"> <OptionsDropdown listId="filter-menu">
@@ -106,6 +106,7 @@ const MainForm = () => {
multiSelect multiSelect
search={{ search={{
search, search,
vins,
online: online ? true : null, online: online ? true : null,
online_hmi: onlineHMI ? true : null, online_hmi: onlineHMI ? true : null,
}} }}

View File

@@ -0,0 +1,74 @@
import { useState, useEffect } from "react";
import { useLocalStorage } from "../../useLocalStorage";
const TYPE_VIN = "vin";
/**
* Match VIN with RegEx
* @param {string} vin - potential VIN
* @returns {boolean}
*/
function isVIN(vin) {
var re = new RegExp("^[A-HJ-NPR-Z\\d]{8}[\\dX][A-HJ-NPR-Z\\d]{2}\\d{6}$");
return vin.match(re)
}
/**
* Convert a string into a QueryPart tuple.
* @typedef {"search" | "vin"} Type - the type of value represented
* @typedef {string} Value - the value
* @typedef {[Type, Value]} QueryPart - a tuple representing a substring of a query
* @param {string} part - substring of a query to turn into a QueryPart
* @returns {QueryPart}
*/
function parseQueryPart(part) {
let type = "search";
if (isVIN(part)) {
type = TYPE_VIN;
part = `${part}`;
}
return [type, part];
}
export default function useQuery() {
const [query, setQuery] = useLocalStorage("VEHICLE_SEARCH", "");
const [parts, setParts] = useState([]);
const [search, setSearch] = useState("");
const [vins, setVins] = useState("");
function reset() {
setSearch("");
setVins([]);
}
useEffect(() => {
reset();
const parts = query.replaceAll(" ", ",").split(",").map(parseQueryPart);
setParts(parts);
parts.forEach(([type, value]) => {
if (type === "vin") {
setVins(vins => {
if (vins.length) {
return `${vins},${value}`;
}
return value;
});
}
if (type === "search") {
setSearch(search => `${search} ${value}`.trim());
}
});
}, [query]);
return {
parts,
search,
vins,
query,
setQuery,
}
}

View File

@@ -0,0 +1,60 @@
import { render, screen, fireEvent } from "@testing-library/react";
import useQuery from "./useQuery";
const MyComponent = () => {
const {
search,
vins,
query,
setQuery,
} = useQuery();
return (
<>
<input
data-testid="input"
value={query}
onChange={(e) => setQuery(e.target.value)}
/>
<ol>
<li data-testid="search">{search}</li>
<li data-testid="vins">{vins}</li>
<li data-testid="query">{query}</li>
</ol>
</>
)
}
const template = (desc, data, expected) => {
it(desc, () => {
render(<MyComponent />);
const input = screen.getByTestId("input");
const search = screen.getByTestId("search");
const vins = screen.getByTestId("vins");
const query = screen.getByTestId("query");
fireEvent.change(input, { target: { value: data } });
expect(search.innerHTML).toBe(expected[0]);
expect(vins.innerHTML).toBe(expected[1]);
expect(query.innerHTML).toBe(data);
});
}
describe("useQuery", () => {
[
["parses a search query", "test", ["test", ""]],
["parses a vin query", "VCF1ZBU23PG001209", ["", "VCF1ZBU23PG001209"]],
["parses a mixed search and vin query", "test VCF1ZBU23PG001209", ["test", "VCF1ZBU23PG001209"]],
["parses a comma separated search query", "ocean,pear,alaska,ronin", ["ocean pear alaska ronin", ""]],
["parses a comma separated vin query", "VCF1EBE2008016235,VCF1EBE20PG001002,VCF1EBE20PG001162", ["", "VCF1EBE2008016235,VCF1EBE20PG001002,VCF1EBE20PG001162"]],
["parses a comma separated mixed search and vin query", "test,VCF1EBE2008016235,VCF1EBE20PG001002,VCF1EBE20PG001162", ["test", "VCF1EBE2008016235,VCF1EBE20PG001002,VCF1EBE20PG001162"]],
["parses a space separated search query", "ocean pear alaska ronin", ["ocean pear alaska ronin", ""]],
["parses a space separated vin query", "VCF1EBE2008016235 VCF1EBE20PG001002 VCF1EBE20PG001162", ["", "VCF1EBE2008016235,VCF1EBE20PG001002,VCF1EBE20PG001162"]],
["parses a space separated mixed search and vin query", "test VCF1EBE2008016235 VCF1EBE20PG001002 VCF1EBE20PG001162", ["test", "VCF1EBE2008016235,VCF1EBE20PG001002,VCF1EBE20PG001162"]],
["trims extraneous values from search", "ocean,, , ,,,,pear,,, ", ["ocean pear", ""]],
["trims extraneous values from vins", "VCF1EBE2008016235,, , ,,,,VCF1EBE20PG001002,,, ", ["", "VCF1EBE2008016235,VCF1EBE20PG001002"]],
].forEach((args) => template(...args))
});