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)}
+ />
+
+ - {search}
+ - {vins}
+ - {query}
+
+ >
+ )
+}
+
+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))
+});