diff --git a/src/components/Battery/index.jsx b/src/components/Battery/index.jsx index 0a9977e..9a5e5d0 100644 --- a/src/components/Battery/index.jsx +++ b/src/components/Battery/index.jsx @@ -19,7 +19,8 @@ const Batteries = [ ]; const chargingStates = [ - "V2L_trunk_active", + ["V2L_trunk_active", "info"], + ["AC_charging", "warning"], ]; function getBatteryByPercent(percent) { @@ -35,7 +36,7 @@ function getBatteryByPercent(percent) { return BatteryFull; } - const unit = 14.2857142857; + const unit = 14.2857142857; // 1/7 * 100 const range = Math.floor(percent / unit); return Batteries[range]; @@ -48,14 +49,16 @@ export default function Battery({ const classes = useStyles(); let Battery = getBatteryByPercent(percent); - if (chargingStates.includes(charge)) { + const charging = chargingStates.find(([state]) => state === charge) || []; + + if (charging[0]) { Battery = BatteryCharging; } return ( -
- {percent && `${percent}%`} +
+ {percent && `${percent}%`}
); diff --git a/src/components/BulkActions/actions/AddToFleet.jsx b/src/components/BulkActions/actions/AddToFleet.jsx deleted file mode 100644 index e3adec5..0000000 --- a/src/components/BulkActions/actions/AddToFleet.jsx +++ /dev/null @@ -1,74 +0,0 @@ -import { useState, forwardRef, useImperativeHandle } from "react"; -import { - FormControl, -} from '@material-ui/core'; -import SearchSelect from "../../SearchSelect/SearchSelect"; -import { useStatusContext } from "../../Contexts/StatusContext"; -import { useUserContext } from "../../Contexts/UserContext"; -import fleetsAPI from "../../../services/fleetsAPI"; - -export default forwardRef(({ - ids, - idCSV, -}, ref) => { - const { setMessage } = useStatusContext(); - const { token: { idToken: { jwtToken: token } } } = useUserContext(); - - const [fleet, setFleet] = useState(null); - - useImperativeHandle(ref, () => ({ - async submit() { - if (!fleet) { - setMessage(`Select a valid fleet, "${fleet}" is invalid.`); - return Promise.reject("Invalid Fleet"); - } - - return fleetsAPI - .addFleetVehicles(fleet, { vins: ids }, token) - .then((response) => { - if (response.error) { - setMessage(`${response.error}: ${response.message}`); - } - - if (response.vins) { - setMessage(`Added ${response.vins.length} vehicles to ${fleet}.`); - return; - } - - setMessage(`Something unexpected happened while attempting to add vehicles to fleet.`); - }) - .catch((error) => { - setMessage(JSON.stringify(error)); - }); - }, - })); - - async function searchFleets(search) { - return fleetsAPI - .getFleets({ - search, - limit: 10, - offset: 0, - order: `id desc`, - }, token) - .then(response => response.data.map(fleet => fleet.name)) - .catch(() => []); - } - - return ( -
-

- You are adding the following VINs to a fleet: {idCSV}. -

- - - -
- ); -}); \ No newline at end of file diff --git a/src/components/BulkActions/actions/UpdateFleetVehicles.jsx b/src/components/BulkActions/actions/UpdateFleetVehicles.jsx new file mode 100644 index 0000000..5682f38 --- /dev/null +++ b/src/components/BulkActions/actions/UpdateFleetVehicles.jsx @@ -0,0 +1,117 @@ +import { useState, forwardRef, useImperativeHandle } from "react"; +import { + FormControl, +} from '@material-ui/core'; +import SearchSelect from "../../SearchSelect/SearchSelect"; +import { useStatusContext } from "../../Contexts/StatusContext"; +import { useUserContext } from "../../Contexts/UserContext"; +import fleetsAPI from "../../../services/fleetsAPI"; + +export default forwardRef(({ + ids, + idCSV, + fleet, +}, ref) => { + const { setMessage } = useStatusContext(); + const { token: { idToken: { jwtToken: token } } } = useUserContext(); + + const [fromFleet, setFromFleet] = useState(fleet); + const [toFleet, setToFleet] = useState(); + + useImperativeHandle(ref, () => ({ + async submit() { + const errorTracking = [false, false]; + + if (toFleet) { + await fleetsAPI + .addFleetVehicles(toFleet, ids, token) + .then((response) => { + if (response.error) { + errorTracking[0] = true; + setMessage(`${response.error}: ${response.message}`); + } + + if (response.vins) { + setMessage(`Added ${response.vins.length} vehicles to ${toFleet}.`); + return; + } + + setMessage(`Something unexpected happened while attempting to add vehicles to fleet.`); + }) + .catch((error) => { + setMessage(JSON.stringify(error)); + return Promise.reject("Could not add vehicles to fleet."); + }); + } + + if (fromFleet) { + await fleetsAPI + .deleteFleetVehicles(fromFleet, ids, token) + .then((response) => { + if (response.error) { + errorTracking[1] = true; + setMessage(`${response.error}: ${response.message}`); + } + + if (response.message === "Deleted") { + setMessage(`Removed ${ids.length} vehicles from ${fromFleet}.`); + return; + } + }) + .catch((error) => { + setMessage(JSON.stringify(error)); + return Promise.reject("Could not remove vehicles from fleet."); + }); + } + + return { + fromFleet, + fromVehicles: errorTracking[1] ? [] : ids, + toFleet, + toVehicles: errorTracking[0] ? [] : ids, + }; + }, + })); + + async function searchFleets(search) { + return fleetsAPI + .getFleets({ + search, + limit: 10, + offset: 0, + order: `id desc`, + }, token) + .then(response => response.data.map(fleet => fleet.name)) + .catch(() => []); + } + + return ( +
+

+ This operation will affect fleet membership for the following VINs: {idCSV}. +

+

+ VINs will be removed from the "From Fleet" and added to the "To Fleet". If you would + like to only add or only remove the other field can be left blank. +

+ + + + + + +
+ ); +}); \ No newline at end of file diff --git a/src/components/BulkActions/actions/AddToFleet.test.jsx b/src/components/BulkActions/actions/UpdateFleetVehicles.test.jsx similarity index 74% rename from src/components/BulkActions/actions/AddToFleet.test.jsx rename to src/components/BulkActions/actions/UpdateFleetVehicles.test.jsx index 3110d92..6003687 100644 --- a/src/components/BulkActions/actions/AddToFleet.test.jsx +++ b/src/components/BulkActions/actions/UpdateFleetVehicles.test.jsx @@ -10,7 +10,7 @@ import { import { UserProvider, setToken } from "../../Contexts/UserContext"; import { StatusProvider } from "../../Contexts/StatusContext"; import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing"; -import AddToFleet from "./AddToFleet"; +import UpdateFleetVehicles from "./UpdateFleetVehicles"; import fleetsAPI from "../../../services/fleetsAPI"; jest.mock('react', () => ({ @@ -23,22 +23,25 @@ jest.mock('@material-ui/core/FormControl', () => { return () =>
; }); -describe("BulkActions/AddToFleet", () => { +describe("BulkActions/UpdateFleetVehicles", () => { beforeAll(() => { setToken(TEST_AUTH_OBJECT_FISKER); }); it("makes request to update the config of multiple vehicles", async () => { useState + .mockReturnValueOnce(["Default-Test", jest.fn()]) + .mockReturnValueOnce([["Default-Test"], jest.fn()]) .mockReturnValueOnce(["Default-Test", jest.fn()]) .mockReturnValueOnce([["Default-Test"], jest.fn()]); - const api = jest.spyOn(fleetsAPI, "addFleetVehicles"); + const add = jest.spyOn(fleetsAPI, "addFleetVehicles"); + const remove = jest.spyOn(fleetsAPI, "deleteFleetVehicles"); const ref = React.createRef(); render( - { ); await act(async () => ref.current.submit()); - expect(api).toHaveBeenCalled(); + expect(add).toHaveBeenCalled(); + expect(remove).toHaveBeenCalled(); }); }); diff --git a/src/components/BulkActions/index.jsx b/src/components/BulkActions/index.jsx index a65e917..33f8de4 100644 --- a/src/components/BulkActions/index.jsx +++ b/src/components/BulkActions/index.jsx @@ -8,17 +8,19 @@ import truncateCSV from "../../utils/truncateCSV"; // Code-splitting individual actions // https://react.dev/reference/react/lazy const AddTags = lazy(() => import("./actions/AddTags")); -const AddToFleet = lazy(() => import("./actions/AddToFleet")); -const UpdateConfig = lazy(() => import("./actions/UpdateConfig")); -const SendSMS = lazy(() => import("./actions/SendSMS")); const Cancel = lazy(() => import("./actions/Cancel")); +const Diagnostic = lazy(() => import("./actions/Diagnostic")); const Redeploy = lazy(() => import("./actions/Redeploy")); const RemoteCommand = lazy(() => import("./actions/RemoteCommand")); -const Diagnostic = lazy(() => import("./actions/Diagnostic")); +const SendSMS = lazy(() => import("./actions/SendSMS")); +const UpdateConfig = lazy(() => import("./actions/UpdateConfig")); +const UpdateFleetVehicles = lazy(() => import("./actions/UpdateFleetVehicles")); export default function BulkActions({ ids = [], actions = [], + fleet = undefined, + callback = (active, ids, context) => { }, // context is raised from the action itself }) { const [open, setOpen] = useState(false); const [title, setTitle] = useState("Action"); @@ -34,30 +36,18 @@ export default function BulkActions({ disabled: false, trigger: () => setActive("addTags"), }, - { - id: "addToFleet", - name: "Add To Fleet", - disabled: false, - trigger: () => setActive("addToFleet"), - }, - { - id: "updateConfig", - name: "Update Config", - disabled: false, - trigger: () => setActive("updateConfig"), - }, - { - id: "sms", - name: "Send SMS", - disabled: !hasRole(groups, Permissions.FiskerCreate, providers), - trigger: () => setActive("sms"), - }, { id: "cancel", name: "Cancel Updates", disabled: false, trigger: () => setActive("cancel"), }, + { + id: "diagnostic", + name: "Send Diagnostic", + disabled: !hasRole(groups, Permissions.CarDiagnostic, providers), + trigger: () => setActive("diagnostic"), + }, { id: "redeploy", name: "Redploy Updates", @@ -72,16 +62,29 @@ export default function BulkActions({ embedded: true, }, { - id: "diagnostic", - name: "Send Diagnostic", - disabled: !hasRole(groups, Permissions.CarDiagnostic, providers), - trigger: () => setActive("diagnostic"), - } + id: "sms", + name: "Send SMS", + disabled: !hasRole(groups, Permissions.FiskerCreate, providers), + trigger: () => setActive("sms"), + }, + { + id: "updateConfig", + name: "Update Config", + disabled: false, + trigger: () => setActive("updateConfig"), + }, + { + id: "updateFleetVehicles", + name: "Move Vehicles", + disabled: false, + trigger: () => setActive("updateFleetVehicles"), + }, ].filter((action) => actions.includes(action.id)); const payload = { ids, idCSV: (ids && ids.length > 0) ? truncateCSV(ids, 10) : "N/A", + fleet, ref: activeRef }; @@ -91,7 +94,7 @@ export default function BulkActions({ const handleSubmit = () => { if (activeRef.current.submit) { - activeRef.current.submit(); + activeRef.current.submit().then((error) => callback(active, ids, error)); } handleClose(); } @@ -117,13 +120,13 @@ export default function BulkActions({ Loading...
}>
{active === "addTags" && } - {active === "addToFleet" && } - {active === "updateConfig" && } - {active === "sms" && } {active === "cancel" && } + {active === "diagnostic" && } {active === "redeploy" && } {active === "remoteCommand" && } - {active === "diagnostic" && } + {active === "sms" && } + {active === "updateConfig" && } + {active === "updateFleetVehicles" && }
diff --git a/src/components/Cars/List/index.jsx b/src/components/Cars/List/index.jsx index 51a617c..5007ccd 100644 --- a/src/components/Cars/List/index.jsx +++ b/src/components/Cars/List/index.jsx @@ -75,7 +75,7 @@ const MainForm = () => { - + diff --git a/src/components/Contexts/FleetContext.jsx b/src/components/Contexts/FleetContext.jsx index 71fcace..30d3cb2 100644 --- a/src/components/Contexts/FleetContext.jsx +++ b/src/components/Contexts/FleetContext.jsx @@ -239,26 +239,39 @@ export const FleetProvider = ({ children }) => { } }; - const deleteFleetVehicle = async (name, vehicle, token) => { + const deleteFleetVehicles = async (name, vins, token) => { + if (!Array.isArray(vins)) { + throw new Error(`VINs are required`); + } + try { setBusy(true); validateFleetName(name); - validateVIN(vehicle.vin); + for (let i = 0; i < vins.length; i++) { + validateVIN(vins[0]); + } - const result = await api.deleteFleetVehicle(name, vehicle, token); - if (result.error) { + const result = await api.deleteFleetVehicles(name, vins, token); + if (result?.error) { throw new Error(`Delete fleet vehicle error. ${result.message}`); } - const index = fleetVehicles.findIndex(element => element === vehicle.vin); - if (index >= 0) fleetVehicles.splice(index, 1); - return result; + removeFleetVehiclesLocal(vins); } finally { setBusy(false); } }; + const removeFleetVehiclesLocal = (vins = []) => { + setFleetVehicles((fleetVehicles) => { + return fleetVehicles.filter((element) => !vins.includes(element.vin)); + }); + setTotalFleetVehicles((totalFleetVehicles) => { + return totalFleetVehicles - vins.length; + }); + } + const getFleetCANFilters = async (name, search, token) => { try { setBusy(true); @@ -358,7 +371,8 @@ export const FleetProvider = ({ children }) => { watchFleetVehicles, getFleetVehicles, addFleetVehicles, - deleteFleetVehicle, + deleteFleetVehicles, + removeFleetVehiclesLocal, fleetCANFilters, totalFleetCANFilters, diff --git a/src/components/Contexts/FleetContext.test.jsx b/src/components/Contexts/FleetContext.test.jsx index ab4bc05..1073f2c 100644 --- a/src/components/Contexts/FleetContext.test.jsx +++ b/src/components/Contexts/FleetContext.test.jsx @@ -465,14 +465,14 @@ describe("FleetContext", () => { }); }); - describe("deleteFleetVehicle", () => { + describe("deleteFleetVehicles", () => { beforeEach(async () => { const TestComp = () => { - const { busy, deleteFleetVehicle } = useFleetContext(); + const { busy, deleteFleetVehicles } = useFleetContext(); const { message, setMessage } = useStatusContext(); - const deleteFV = async (name, vehicle) => { + const deleteFV = async (name, vins) => { try { - await deleteFleetVehicle(name, vehicle); + await deleteFleetVehicles(name, vins); } catch (e) { setMessage(e.message); } @@ -488,11 +488,11 @@ describe("FleetContext", () => { />