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 = ({