From 7c358a6052601d8bed4fb9a2da97c083f44dfdf2 Mon Sep 17 00:00:00 2001 From: Tristan Timblin Date: Fri, 16 Jun 2023 11:48:48 -0700 Subject: [PATCH] CEC-4525: add support for /tags endpoint and implement a new action for it (#361) * add action for adding tags --- src/components/App/App.test.js | 1 + .../App/__snapshots__/App.test.js.snap | 6 ++ src/components/Cars/List/index.jsx | 41 ++++++-- src/components/Contexts/VehicleContext.jsx | 17 ++++ .../__snapshots__/index.test.jsx.snap | 58 +++++++++++ .../Controls/TextInputList/index.jsx | 97 +++++++++++++++++++ .../Controls/TextInputList/index.test.jsx | 57 +++++++++++ .../TransformModal/__mocks__/index.jsx | 5 + src/components/TransformModal/index.jsx | 24 ++++- src/services/vehiclesAPI.js | 12 +++ 10 files changed, 308 insertions(+), 10 deletions(-) create mode 100644 src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap create mode 100644 src/components/Controls/TextInputList/index.jsx create mode 100644 src/components/Controls/TextInputList/index.test.jsx create mode 100644 src/components/TransformModal/__mocks__/index.jsx diff --git a/src/components/App/App.test.js b/src/components/App/App.test.js index f665b6e..7d64b94 100644 --- a/src/components/App/App.test.js +++ b/src/components/App/App.test.js @@ -9,6 +9,7 @@ jest.mock("../../services/vehiclesAPI"); jest.mock("../../services/superset"); jest.mock("../../services/suppliersAPI"); jest.mock("../../services/issueAPI"); +jest.mock("../TransformModal"); import { act, cleanup, render, diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap index 28e0e1e..7205472 100644 --- a/src/components/App/__snapshots__/App.test.js.snap +++ b/src/components/App/__snapshots__/App.test.js.snap @@ -12652,6 +12652,12 @@ exports[`App Route /vehicles authenticated 1`] = ` +
+
diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index f4a332d..41485da 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -30,8 +30,15 @@ const MainForm = () => { type: "boolean", value: false }, - }) - const [showUpdateConfigModal, setShowUpdateConfigModal] = useState(false); + }); + const [tagsToAdd, setTagsToAdd] = useState({ + tags: { + label: "Tags", + type: "list.string", + value: [], + }, + }); + const [activeModal, setActiveModal] = useState(null); const { setTitle, setSitePath, setMessage } = useStatusContext(); const { token: { @@ -80,12 +87,23 @@ const MainForm = () => { selectedVins.forEach((vin, i) => taskRunner.push(request(vin, i))) } + const handleAddTags = async (fn) => { + await fn(selectedVins, tagsToAdd.tags.value, token) + .then(() => setMessage(`Added ${tagsToAdd.tags.value.length} tags to ${selectedVins.length} vehicles.`)) + .catch((error) => setMessage(error.message)); + }; + const actions = [ { name: "Update Configs", disabled: selectedVins.length === 0, - trigger: () => setShowUpdateConfigModal(true), + trigger: () => setActiveModal("updateConfig"), }, + { + name: "Add Tags", + disabled: selectedVins.length === 0, + trigger: () => setActiveModal("addTags"), + } ]; const handleOnlineHMI = (event) => { @@ -150,17 +168,26 @@ const MainForm = () => { onSelectAll={handleSelectAll} /> - {(context) => ( + {(context) => (<> setShowUpdateConfigModal(false)} + open={activeModal === "updateConfig"} + close={() => setActiveModal(null)} title="Update Configs" body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`} data={config} setData={setConfig} submit={() => handleUploadConfig(context.uploadConfig)} /> - )} + setActiveModal(null)} + title="Add Tags" + body={`You are adding tags for the following VINs: ${selectedVins.join(", ")}.`} + data={tagsToAdd} + setData={setTagsToAdd} + submit={() => handleAddTags(context.addTags)} + /> + )} ); diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index 6a45be8..7bb2fae 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -61,6 +61,22 @@ export const VehicleProvider = ({ children }) => { } }; + const addTags = async (vins, tags, token) => { + try { + setBusy(true); + vins.forEach(vin => validateVIN(vin)); + const validateTags = tags.every(tag => typeof tag === "string"); + if (!validateTags) + throw new Error("Invalid Tag"); + + const result = await api.addTags(vins, tags, token) + if (result.error) + throw new Error(`Add tags error. ${result.message}`); + } finally { + setBusy(false) + } + } + const getConnections = async (vins, token) => { try { setBusy(true); @@ -301,6 +317,7 @@ export const VehicleProvider = ({ children }) => { getFleets, getVersionLog, uploadConfig, + addTags, }} > {children} diff --git a/src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap b/src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..d1eb692 --- /dev/null +++ b/src/components/Controls/TextInputList/__snapshots__/index.test.jsx.snap @@ -0,0 +1,58 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DropDownButton Render 1`] = ` +
+
+
+ +
+ + +
+
+
+
+
+`; diff --git a/src/components/Controls/TextInputList/index.jsx b/src/components/Controls/TextInputList/index.jsx new file mode 100644 index 0000000..a3fcb94 --- /dev/null +++ b/src/components/Controls/TextInputList/index.jsx @@ -0,0 +1,97 @@ +import { useState, useEffect } from "react"; +import { Cancel } from "@mui/icons-material"; +import { TextField } from "@mui/material"; +import { Box } from "@mui/system"; +import { useStatusContext } from "../../Contexts/StatusContext"; + +const TextInput = ({ text, handleDelete }) => { + return ( + + {text} + handleDelete(text)} + /> + + ); +} + +const TextInputList = ({ + onChange = () => {}, + validate = () => {}, + label +}) => { + const [textList, setTextList] = useState([]); + const [input, setInput] = useState(""); + + const { setMessage } = useStatusContext(); + + const handleDelete = (textToDelete) => { + setTextList(textList => textList.filter(text => text !== textToDelete)); + } + + const handleOnChange = (event) => { + const char = event.nativeEvent.data; + if (char === ",") { + try { + if (validate) validate(input); + setTextList(textList => [...textList, input]); + setInput(""); + } catch { + setMessage(`"${input}" is not valid.`); + } + } else { + setInput(event.target.value); + } + } + + useEffect(() => { + onChange(input.length ? [...textList, input] : [...textList]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [textList, input]); + + return ( +
+ + + + {textList.map((text) => ( + + ))} + +
+ ); +}; + +export default TextInputList; \ No newline at end of file diff --git a/src/components/Controls/TextInputList/index.test.jsx b/src/components/Controls/TextInputList/index.test.jsx new file mode 100644 index 0000000..4ce17b1 --- /dev/null +++ b/src/components/Controls/TextInputList/index.test.jsx @@ -0,0 +1,57 @@ +jest.mock("../../Contexts/StatusContext"); + +import React from "react"; +import { render, waitFor } from "@testing-library/react"; +import userEvent from '@testing-library/user-event'; + +import TextInputList from "."; +import addSnapshotSerializer from "../../../utils/snapshot"; + +describe("DropDownButton", () => { + beforeAll(() => { + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + const { container } = render( + + ); + await waitFor(() => { + /* render */ + }); + expect(container).toMatchSnapshot(); + }); + + it("properly adds tag after comma", async () => { + const { getByText, getByTestId } = render( + + ); + + const [inputEl] = getByTestId("text-input-list").getElementsByTagName("input"); + userEvent.type(inputEl, "tag1"); + userEvent.type(inputEl, ","); + expect(getByText("tag1").nodeName).toBe("DIV"); + }); + + it("properly passes payload to callback", async () => { + const mockCallback = jest.fn(); + + const { getByTestId } = render( + + ); + + const [inputEl] = getByTestId("text-input-list").getElementsByTagName("input"); + userEvent.type(inputEl, "tag1"); + userEvent.type(inputEl, ","); + userEvent.type(inputEl, "tag2"); + expect(mockCallback).toHaveBeenCalledWith(["tag1", "tag2"]); + }); +}); diff --git a/src/components/TransformModal/__mocks__/index.jsx b/src/components/TransformModal/__mocks__/index.jsx new file mode 100644 index 0000000..dc2966e --- /dev/null +++ b/src/components/TransformModal/__mocks__/index.jsx @@ -0,0 +1,5 @@ +const TransformModalMock = jest.fn().mockImplementation(() => { + return
+}); + +export default TransformModalMock; diff --git a/src/components/TransformModal/index.jsx b/src/components/TransformModal/index.jsx index 7aae75b..8a58a0c 100644 --- a/src/components/TransformModal/index.jsx +++ b/src/components/TransformModal/index.jsx @@ -10,6 +10,7 @@ import { FormGroup, FormControlLabel, } from '@material-ui/core'; +import TextInputList from "../Controls/TextInputList"; const TransformModal = ({ open, @@ -25,16 +26,24 @@ const TransformModal = ({ submit(); }; - const handleChange = (key) => { + const handleChange = (key, value) => { setData((data) => { const {[key]: toChange, ...rest} = data; - toChange.value = !toChange.value; + switch (data[key].type) { + case "boolean": + toChange.value = !toChange.value; + break; + case "list.string": + toChange.value = value; + break; + default: + } return { [key]: toChange, ...rest }; }); - } + }; return ( ) + case "list.string": + return ( + handleChange(key, list)} + /> + ) default: return <>; } @@ -72,6 +89,7 @@ const TransformModal = ({