diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index c0ab155..05b5cc7 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -7,19 +7,31 @@ import { Link } from "react-router-dom"; import { Permissions } from "../../../utils/roles"; import { useStatusContext } from "../../Contexts/StatusContext"; import { useUserContext } from "../../Contexts/UserContext"; -import { VehicleProvider } from "../../Contexts/VehicleContext"; +import { VehicleProvider, VehicleContext } from "../../Contexts/VehicleContext"; import CarSelectionTable from "../../Controls/CarSelectionTable"; import OptionsDropdown from "../../Controls/OptionsDropdown"; import { RoleWrap } from "../../Controls/RoleWrap"; import SearchField from "../../Controls/SearchField"; +import DropDownButton from "../../Controls/DropDownButton"; +import TransformModal from "../../TransformModal"; import useStyles from "../../useStyles"; +import TaskRunner from "../../../utils/taskRunner"; const MainForm = () => { const classes = useStyles(); const [search, setSearch] = useState(""); const [online, setOnline] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false); - const { setTitle, setSitePath } = useStatusContext(); + const [selectedVins, setSelectedVins] = useState([]); + const [config, setConfig] = useState({ + force: { + label: "Force push", + type: "boolean", + value: false + }, + }) + const [showUpdateConfigModal, setShowUpdateConfigModal] = useState(false); + const { setTitle, setSitePath, setMessage } = useStatusContext(); const { token: { idToken: { jwtToken: token }, @@ -36,6 +48,45 @@ const MainForm = () => { setOnline(event.target.checked); }; + const handleSelectAll = (cars) => { + setSelectedVins(cars); + }; + + const handleSelect = (event, key) => { + setSelectedVins((selectedVins) => { + if (event.target.checked) { + return [...selectedVins, key]; + } + return selectedVins.filter(vin => vin !== key); + }); + }; + + const handleUploadConfig = (fn) => { + const taskRunner = new TaskRunner(5); + const request = (vin, i) => { + const messagePrefix = `${i+1}/${selectedVins.length} "${vin}":`; + return async () => { + const result = await fn(vin, config.force.value, token) + .then(() => { + setMessage(`${messagePrefix} updated.`); + }) + .catch((error) => { + setMessage(`${messagePrefix} ${error.message}`); + }); + return result; + } + } + selectedVins.forEach((vin, i) => taskRunner.push(request(vin, i))) + } + + const actions = [ + { + name: "Update Configs", + disabled: selectedVins.length === 0, + trigger: () => setShowUpdateConfigModal(true), + }, + ]; + const handleOnlineHMI = (event) => { setOnlineHMI(event.target.checked); }; @@ -49,7 +100,7 @@ const MainForm = () => { return (
- + { + - + { + + {(context) => ( + setShowUpdateConfigModal(false)} + title="Update Configs" + body={`You are updating the config for the following VINs: ${selectedVins.join(", ")}.`} + data={config} + setData={setConfig} + submit={() => handleUploadConfig(context.uploadConfig)} + /> + )} +
); }; diff --git a/src/components/Contexts/VehicleContext.jsx b/src/components/Contexts/VehicleContext.jsx index 2486828..6967a34 100644 --- a/src/components/Contexts/VehicleContext.jsx +++ b/src/components/Contexts/VehicleContext.jsx @@ -4,7 +4,7 @@ import { logger } from "../../services/monitoring"; import api from "../../services/vehiclesAPI"; import { validateVIN } from "../../utils/validationSupplier"; -const VehicleContext = React.createContext(); +export const VehicleContext = React.createContext(); const validateAdd = (vehicle) => { if (vehicle == null) { @@ -299,7 +299,7 @@ export const VehicleProvider = ({ children }) => { updateVehicle, getFleets, getVersionLog, - uploadConfig + uploadConfig, }} > {children} diff --git a/src/components/Controls/DropDownButton/__snapshots__/index.test.jsx.snap b/src/components/Controls/DropDownButton/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..a4d6165 --- /dev/null +++ b/src/components/Controls/DropDownButton/__snapshots__/index.test.jsx.snap @@ -0,0 +1,103 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DownloadFileLink Render 1`] = ` +
+
+ + +
+
+`; + +exports[`DropDownButton Render 1`] = ` +
+
+ + +
+
+`; diff --git a/src/components/Controls/DropDownButton/index.jsx b/src/components/Controls/DropDownButton/index.jsx new file mode 100644 index 0000000..77a5562 --- /dev/null +++ b/src/components/Controls/DropDownButton/index.jsx @@ -0,0 +1,109 @@ +import { useRef, useState } from "react"; +import { + Button, + ButtonGroup, + ClickAwayListener, + Grow, + MenuItem, + MenuList, + Paper, + Popper, +} from "@mui/material"; +import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; + +const DropDownButton = ({ actions = [], payload = [] }) => { + const [open, setOpen] = useState(false); + const [selectedIndex, setSelectedIndex] = useState(0); + const anchorRef = useRef(null); + + const handleClick = () => { + actions[selectedIndex].trigger(...payload); + }; + + const handleMenuItemClick = (event, index) => { + setSelectedIndex(index); + setOpen(false); + } + + const handleToggle = () => { + setOpen(open => !open); + }; + + const handleClose = (event) => { + if ( + anchorRef.current && + anchorRef.current.contains(event.target) + ) { + return; + } + + setOpen(false); + }; + + return ( + <> + + + + + + {({ TransitionProps, placement }) => ( + + + + + {actions.map((action, index) => ( + handleMenuItemClick(event, index)} + > + {action.name} + + ))} + + + + + )} + + + ) +} + +export default DropDownButton; \ No newline at end of file diff --git a/src/components/Controls/DropDownButton/index.test.jsx b/src/components/Controls/DropDownButton/index.test.jsx new file mode 100644 index 0000000..570c51f --- /dev/null +++ b/src/components/Controls/DropDownButton/index.test.jsx @@ -0,0 +1,80 @@ +import React from "react"; +import { fireEvent, render, waitFor, screen } from "@testing-library/react"; + +import DropDownButton from "."; +import addSnapshotSerializer from "../../../utils/snapshot"; + +describe("DropDownButton", () => { + beforeAll(() => { + addSnapshotSerializer(expect); + }); + + it("Render", async () => { + const actions = [ + { + name: "Action One", + disabled: false, + trigger: (paramOne, paramTwo) => {} + }, + { + name: "Action Two", + disabled: false, + trigger: (paramOne, paramTwo) => {} + }, + ]; + + const { container } = render( + + ); + await waitFor(() => { + /* render */ + }); + expect(container).toMatchSnapshot(); + }); + + it("properly disables an action", async () => { + const actions = [ + { + name: "Disabled Action", + disabled: true, + trigger: () => {} + }, + ]; + + const { getByText } = render( + + ); + await waitFor(() => { + /* render */ + }); + const buttonEl = getByText("Disabled Action").parentElement; + expect(buttonEl).toHaveProperty("disabled", true); + }); + + it("properly passes payload to callback", async () => { + const actions = [ + { + name: "Action One", + disabled: false, + trigger: jest.fn(), + }, + ]; + + render( + + ); + + const buttonEl = screen.getByText("Action One"); + fireEvent.click(buttonEl); + expect(actions[0].trigger).toHaveBeenCalledWith("somePayload", "somePayload2"); + }); +}); diff --git a/src/components/TransformModal/__snapshots__/index.test.jsx.snap b/src/components/TransformModal/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..c2a5f3f --- /dev/null +++ b/src/components/TransformModal/__snapshots__/index.test.jsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TransformModal Render 1`] = ` +