CEC-4525: add support for /tags endpoint and implement a new action for it (#361)
* add action for adding tags
This commit is contained in:
@@ -9,6 +9,7 @@ jest.mock("../../services/vehiclesAPI");
|
|||||||
jest.mock("../../services/superset");
|
jest.mock("../../services/superset");
|
||||||
jest.mock("../../services/suppliersAPI");
|
jest.mock("../../services/suppliersAPI");
|
||||||
jest.mock("../../services/issueAPI");
|
jest.mock("../../services/issueAPI");
|
||||||
|
jest.mock("../TransformModal");
|
||||||
|
|
||||||
import {
|
import {
|
||||||
act, cleanup, render,
|
act, cleanup, render,
|
||||||
|
|||||||
@@ -12652,6 +12652,12 @@ exports[`App Route /vehicles authenticated 1`] = `
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
data-testid="transform-modal"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
data-testid="transform-modal"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -30,8 +30,15 @@ const MainForm = () => {
|
|||||||
type: "boolean",
|
type: "boolean",
|
||||||
value: false
|
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 { setTitle, setSitePath, setMessage } = useStatusContext();
|
||||||
const {
|
const {
|
||||||
token: {
|
token: {
|
||||||
@@ -80,12 +87,23 @@ const MainForm = () => {
|
|||||||
selectedVins.forEach((vin, i) => taskRunner.push(request(vin, i)))
|
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 = [
|
const actions = [
|
||||||
{
|
{
|
||||||
name: "Update Configs",
|
name: "Update Configs",
|
||||||
disabled: selectedVins.length === 0,
|
disabled: selectedVins.length === 0,
|
||||||
trigger: () => setShowUpdateConfigModal(true),
|
trigger: () => setActiveModal("updateConfig"),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Add Tags",
|
||||||
|
disabled: selectedVins.length === 0,
|
||||||
|
trigger: () => setActiveModal("addTags"),
|
||||||
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
const handleOnlineHMI = (event) => {
|
const handleOnlineHMI = (event) => {
|
||||||
@@ -150,17 +168,26 @@ const MainForm = () => {
|
|||||||
onSelectAll={handleSelectAll}
|
onSelectAll={handleSelectAll}
|
||||||
/>
|
/>
|
||||||
<VehicleConsumer>
|
<VehicleConsumer>
|
||||||
{(context) => (
|
{(context) => (<>
|
||||||
<TransformModal
|
<TransformModal
|
||||||
open={showUpdateConfigModal}
|
open={activeModal === "updateConfig"}
|
||||||
close={() => setShowUpdateConfigModal(false)}
|
close={() => setActiveModal(null)}
|
||||||
title="Update Configs"
|
title="Update Configs"
|
||||||
body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`}
|
body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`}
|
||||||
data={config}
|
data={config}
|
||||||
setData={setConfig}
|
setData={setConfig}
|
||||||
submit={() => handleUploadConfig(context.uploadConfig)}
|
submit={() => handleUploadConfig(context.uploadConfig)}
|
||||||
/>
|
/>
|
||||||
)}
|
<TransformModal
|
||||||
|
open={activeModal === "addTags"}
|
||||||
|
close={() => setActiveModal(null)}
|
||||||
|
title="Add Tags"
|
||||||
|
body={`You are adding tags for the following VINs: ${selectedVins.join(", ")}.`}
|
||||||
|
data={tagsToAdd}
|
||||||
|
setData={setTagsToAdd}
|
||||||
|
submit={() => handleAddTags(context.addTags)}
|
||||||
|
/>
|
||||||
|
</>)}
|
||||||
</VehicleConsumer>
|
</VehicleConsumer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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) => {
|
const getConnections = async (vins, token) => {
|
||||||
try {
|
try {
|
||||||
setBusy(true);
|
setBusy(true);
|
||||||
@@ -301,6 +317,7 @@ export const VehicleProvider = ({ children }) => {
|
|||||||
getFleets,
|
getFleets,
|
||||||
getVersionLog,
|
getVersionLog,
|
||||||
uploadConfig,
|
uploadConfig,
|
||||||
|
addTags,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`DropDownButton Render 1`] = `
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
class="MuiFormControl-root MuiFormControl-marginNormal MuiFormControl-fullWidth MuiTextField-root css-17vbkzs-MuiFormControl-root-MuiTextField-root"
|
||||||
|
data-testid="text-input-list"
|
||||||
|
>
|
||||||
|
<label
|
||||||
|
class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeSmall MuiInputLabel-outlined MuiFormLabel-colorPrimary Mui-required MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-sizeSmall MuiInputLabel-outlined css-1pysi21-MuiFormLabel-root-MuiInputLabel-root"
|
||||||
|
data-shrink="false"
|
||||||
|
for="mui-0"
|
||||||
|
id="mui-0-label"
|
||||||
|
>
|
||||||
|
The input label (use commas to add multiple)
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiFormLabel-asterisk MuiInputLabel-asterisk css-wgai2y-MuiFormLabel-asterisk"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<div
|
||||||
|
class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-colorPrimary MuiInputBase-fullWidth MuiInputBase-formControl MuiInputBase-sizeSmall css-md26zr-MuiInputBase-root-MuiOutlinedInput-root"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
aria-invalid="false"
|
||||||
|
class="MuiInputBase-input MuiOutlinedInput-input MuiInputBase-inputSizeSmall css-1n4twyu-MuiInputBase-input-MuiOutlinedInput-input"
|
||||||
|
id="mui-0"
|
||||||
|
name="text"
|
||||||
|
required=""
|
||||||
|
type="text"
|
||||||
|
value=""
|
||||||
|
/>
|
||||||
|
<fieldset
|
||||||
|
aria-hidden="true"
|
||||||
|
class="MuiOutlinedInput-notchedOutline css-1d3z3hw-MuiOutlinedInput-notchedOutline"
|
||||||
|
>
|
||||||
|
<legend
|
||||||
|
class="css-yjsfm1"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
The input label (use commas to add multiple)
|
||||||
|
|
||||||
|
*
|
||||||
|
</span>
|
||||||
|
</legend>
|
||||||
|
</fieldset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="MuiBox-root css-6km64s"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
97
src/components/Controls/TextInputList/index.jsx
Normal file
97
src/components/Controls/TextInputList/index.jsx
Normal file
@@ -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 (
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "2px",
|
||||||
|
p: "2px 4px",
|
||||||
|
backgroundColor: "#ccc",
|
||||||
|
borderRadius: 1,
|
||||||
|
}}>
|
||||||
|
{text}
|
||||||
|
<Cancel
|
||||||
|
sx={{
|
||||||
|
color: "#666",
|
||||||
|
fontSize: "16px",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
onClick={() => handleDelete(text)}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
data-testid="text-input-list"
|
||||||
|
name="text"
|
||||||
|
label={`${label} (use commas to add multiple)`}
|
||||||
|
value={input}
|
||||||
|
variant="outlined"
|
||||||
|
margin="normal"
|
||||||
|
required
|
||||||
|
size="small"
|
||||||
|
fullWidth
|
||||||
|
onChange={handleOnChange}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Box sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: 1,
|
||||||
|
pr: 1,
|
||||||
|
}}>
|
||||||
|
{textList.map((text) => (
|
||||||
|
<TextInput
|
||||||
|
text={text}
|
||||||
|
handleDelete={handleDelete}
|
||||||
|
key={text}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TextInputList;
|
||||||
57
src/components/Controls/TextInputList/index.test.jsx
Normal file
57
src/components/Controls/TextInputList/index.test.jsx
Normal file
@@ -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(
|
||||||
|
<TextInputList
|
||||||
|
label={"The input label"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
await waitFor(() => {
|
||||||
|
/* render */
|
||||||
|
});
|
||||||
|
expect(container).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("properly adds tag after comma", async () => {
|
||||||
|
const { getByText, getByTestId } = render(
|
||||||
|
<TextInputList
|
||||||
|
label={"The input label"}
|
||||||
|
payload={[]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<TextInputList
|
||||||
|
label={"The input label"}
|
||||||
|
onChange={mockCallback}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const [inputEl] = getByTestId("text-input-list").getElementsByTagName("input");
|
||||||
|
userEvent.type(inputEl, "tag1");
|
||||||
|
userEvent.type(inputEl, ",");
|
||||||
|
userEvent.type(inputEl, "tag2");
|
||||||
|
expect(mockCallback).toHaveBeenCalledWith(["tag1", "tag2"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
5
src/components/TransformModal/__mocks__/index.jsx
Normal file
5
src/components/TransformModal/__mocks__/index.jsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
const TransformModalMock = jest.fn().mockImplementation(() => {
|
||||||
|
return <div data-testid="transform-modal" />
|
||||||
|
});
|
||||||
|
|
||||||
|
export default TransformModalMock;
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
FormGroup,
|
FormGroup,
|
||||||
FormControlLabel,
|
FormControlLabel,
|
||||||
} from '@material-ui/core';
|
} from '@material-ui/core';
|
||||||
|
import TextInputList from "../Controls/TextInputList";
|
||||||
|
|
||||||
const TransformModal = ({
|
const TransformModal = ({
|
||||||
open,
|
open,
|
||||||
@@ -25,16 +26,24 @@ const TransformModal = ({
|
|||||||
submit();
|
submit();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleChange = (key) => {
|
const handleChange = (key, value) => {
|
||||||
setData((data) => {
|
setData((data) => {
|
||||||
const {[key]: toChange, ...rest} = 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 {
|
return {
|
||||||
[key]: toChange,
|
[key]: toChange,
|
||||||
...rest
|
...rest
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
@@ -64,6 +73,14 @@ const TransformModal = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
|
case "list.string":
|
||||||
|
return (
|
||||||
|
<TextInputList
|
||||||
|
key={key}
|
||||||
|
label={value.label}
|
||||||
|
onChange={(list) => handleChange(key, list)}
|
||||||
|
/>
|
||||||
|
)
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
@@ -72,6 +89,7 @@ const TransformModal = ({
|
|||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button
|
<Button
|
||||||
|
label="Test"
|
||||||
onClick={close}
|
onClick={close}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
|
|||||||
@@ -17,6 +17,18 @@ const vehiclesAPI = {
|
|||||||
.then(fetchRespHandler)
|
.then(fetchRespHandler)
|
||||||
.catch(errorHandler),
|
.catch(errorHandler),
|
||||||
|
|
||||||
|
addTags: async (vins, tags, token) =>
|
||||||
|
fetch(`${API_ENDPOINT}/tags`, {
|
||||||
|
method: "PUT",
|
||||||
|
headers: Object.assign(
|
||||||
|
{ "Content-Type": "application/json" },
|
||||||
|
getAuthHeaderOptions(token),
|
||||||
|
),
|
||||||
|
body: JSON.stringify({ vins, tags }),
|
||||||
|
})
|
||||||
|
.then(fetchRespHandler)
|
||||||
|
.catch(errorHandler),
|
||||||
|
|
||||||
deleteVehicle: async (vin, token) =>
|
deleteVehicle: async (vin, token) =>
|
||||||
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
|
fetch(`${API_ENDPOINT}/vehicle/${vin}`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
|
|||||||
Reference in New Issue
Block a user