From 842c402d05cd1da1562fdcfe92e9caf85324ada3 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Fri, 22 Sep 2023 10:53:58 -0700 Subject: [PATCH] CEC-5085: add search on VINs support (#450) --- src/components/Cars/List/index.jsx | 9 +-- src/components/Cars/List/useQuery.js | 74 ++++++++++++++++++++++ src/components/Cars/List/useQuery.test.jsx | 60 ++++++++++++++++++ 3 files changed, 139 insertions(+), 4 deletions(-) create mode 100644 src/components/Cars/List/useQuery.js create mode 100644 src/components/Cars/List/useQuery.test.jsx diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index 7fa7bd8..15b1ba4 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -13,15 +13,15 @@ import OptionsDropdown from "../../Controls/OptionsDropdown"; import { RoleWrap } from "../../Controls/RoleWrap"; import SearchField from "../../Controls/SearchField"; import BulkActions from "../../BulkActions"; -import { useLocalStorage } from "../../useLocalStorage"; +import useQuery from "./useQuery"; import useStyles from "../../useStyles"; const MainForm = () => { const classes = useStyles(); - const [search, setSearch] = useLocalStorage("VEHICLE_SEARCH", ""); const [online, setOnline] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false); const [selectedVins, setSelectedVins] = useState([]); + const { vins, search, query, setQuery } = useQuery(); const { setTitle, setSitePath } = useStatusContext(); const { token: { @@ -32,7 +32,7 @@ const MainForm = () => { } = useUserContext(); const handleSearch = (query) => { - setSearch(query); + setQuery(query); }; const handleOnline = (event) => { @@ -78,7 +78,7 @@ const MainForm = () => { - + @@ -106,6 +106,7 @@ const MainForm = () => { multiSelect search={{ search, + vins, online: online ? true : null, online_hmi: onlineHMI ? true : null, }} diff --git a/src/components/Cars/List/useQuery.js b/src/components/Cars/List/useQuery.js new file mode 100644 index 0000000..715c82b --- /dev/null +++ b/src/components/Cars/List/useQuery.js @@ -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, + } +} diff --git a/src/components/Cars/List/useQuery.test.jsx b/src/components/Cars/List/useQuery.test.jsx new file mode 100644 index 0000000..14f7483 --- /dev/null +++ b/src/components/Cars/List/useQuery.test.jsx @@ -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 ( + <> + setQuery(e.target.value)} + /> +
    +
  1. {search}
  2. +
  3. {vins}
  4. +
  5. {query}
  6. +
+ + ) +} + +const template = (desc, data, expected) => { + it(desc, () => { + render(); + 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)) +});