CEC-5085: add search on VINs support (#450)
This commit is contained in:
@@ -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,
|
||||||
}}
|
}}
|
||||||
|
|||||||
74
src/components/Cars/List/useQuery.js
Normal file
74
src/components/Cars/List/useQuery.js
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
60
src/components/Cars/List/useQuery.test.jsx
Normal file
60
src/components/Cars/List/useQuery.test.jsx
Normal 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))
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user