CEC-4640: add bulk add to fleet (#384)

* refactor bulkactions component

* refactor bulk actions

* update dom tests

* add addToFleet hook

* make signal optional

* implement code splitting

* add deps

* remove test label
This commit is contained in:
Tristan Timblin
2023-07-10 17:30:11 -04:00
committed by GitHub
parent db88d5eba1
commit 754e445c09
33 changed files with 739 additions and 437 deletions

View File

@@ -0,0 +1,43 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import TextInputList from "../../Controls/TextInputList";
import vehiclesAPI from "../../../services/vehiclesAPI";
export default forwardRef(({
vins,
vinCSV,
}, ref) => {
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const [tags, setTags] = useState([]);
useImperativeHandle(ref, () => ({
async submit() {
return vehiclesAPI
.addTags(vins, tags, token)
.then((data) => {
if (data.error) {
setMessage(`${data.error}: ${data.message}`);
} else if (data.tags && data.vins) {
setMessage(`Added ${data.tags.length} tags to ${data.vins.length} vehicles.`);
} else {
setMessage(JSON.stringify(data));
}
});
},
}));
return (
<div>
<p>
You are adding tags to the following VINs: {vinCSV}.
</p>
<TextInputList
label="Tags"
onChange={setTags}
/>
</div>
);
});

View File

@@ -0,0 +1,51 @@
jest.mock("../../Contexts/UserContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../../services/vehiclesAPI");
import React, { useState } from "react";
import {
render,
act,
} from "@testing-library/react";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
import AddTags from "./AddTags";
import vehiclesAPI from "../../../services/vehiclesAPI";
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));
jest.mock('../../Controls/TextInputList', () => {
const React = require('react');
return () => <div data-testid="mock-text-input-list" />;
});
describe("BulkActions/AddTags", () => {
beforeAll(() => {
setToken(TEST_AUTH_OBJECT_FISKER);
});
it("makes request to update the config of multiple vehicles", async () => {
useState.mockReturnValue([["myTag"], jest.fn()]);
const api = jest.spyOn(vehiclesAPI, "addTags");
const ref = React.createRef();
render(
<StatusProvider>
<UserProvider>
<AddTags
ref={ref}
vins={["TESTVIN123456789a"]}
vinCSV=""
/>
</UserProvider>
</StatusProvider>
);
await act(async () => ref.current.submit());
expect(api).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,99 @@
import { useEffect, useState, forwardRef, useImperativeHandle } from "react";
import {
FormControl,
InputLabel,
MenuItem,
Select,
} from '@material-ui/core';
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import fleetsAPI from "../../../services/fleetsAPI";
export default forwardRef(({
vins,
vinCSV,
}, ref) => {
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const [fleet, setFleet] = useState("");
const [options, setOptions] = useState([]);
useImperativeHandle(ref, () => ({
async submit() {
if (!fleet) {
setMessage(`Select a valid fleet, "${fleet}" is invalid.`);
return Promise.reject("Invalid Fleet");
}
return fleetsAPI
.addFleetVehicles(fleet, { vins }, 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));
});
},
}));
useEffect(() => {
const controller = new AbortController();
let isMounted = true;
fleetsAPI
.getFleets({
search: "",
limit: 10,
offset: 0,
order: `id desc`,
}, token, controller)
.then(({ data }) => {
if (isMounted) {
setOptions(data.map((fleet) => fleet.name));
}
});
return () => {
controller?.abort();
isMounted = false;
}
}, [token]);
const handleChange = (event) => {
setFleet(event.target.value);
}
return (
<div>
<p>
You are adding the following VINs to a fleet: {vinCSV}.
</p>
{options && (
<FormControl variant="filled" fullWidth={true}>
<InputLabel id="fleet-selection">
Fleet
</InputLabel>
<Select
labelId="fleet-selection"
value={fleet}
label="Fleet"
onChange={handleChange}
>
{options.map((option) => (
<MenuItem key={option} value={option}>{option}</MenuItem>
))}
</Select>
</FormControl>
)}
</div>
);
});

View File

@@ -0,0 +1,53 @@
jest.mock("../../Contexts/UserContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../../services/fleetsAPI");
import React, { useEffect, useState } from "react";
import {
render,
act,
} from "@testing-library/react";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
import AddToFleet from "./AddToFleet";
import fleetsAPI from "../../../services/fleetsAPI";
jest.mock('react', () => ({
...jest.requireActual('react'),
useState: jest.fn(),
}));
jest.mock('@material-ui/core/FormControl', () => {
const React = require('react');
return () => <div data-testid="mock-form-control" />;
});
describe("BulkActions/AddToFleet", () => {
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()]);
const api = jest.spyOn(fleetsAPI, "addFleetVehicles");
const ref = React.createRef();
render(
<StatusProvider>
<UserProvider>
<AddToFleet
ref={ref}
vins={["TESTVIN1234567890"]}
vinCSV=""
/>
</UserProvider>
</StatusProvider>
);
await act(async () => ref.current.submit());
expect(api).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,55 @@
import { forwardRef, useImperativeHandle } from "react";
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import TaskRunner from "../../../utils/taskRunner";
import vehiclesAPI from "../../../services/vehiclesAPI";
export default forwardRef(({
vins,
vinCSV,
}, ref) => {
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
useImperativeHandle(ref, () => ({
async submit() {
return new Promise((resolve, reject) => {
const taskRunner = new TaskRunner(5, vins.length);
let errorCount = 0;
const task = (vin, index) => {
const progressMessage = `${index + 1}/${vins.length}`;
return async () => vehiclesAPI.deleteVehicle(vin, token)
.then((response) => {
if (response.error) {
errorCount += 1;
setMessage(`${progressMessage} ${response.error}: ${response.message}`);
} else {
setMessage(`${progressMessage} Deleted ${vin}`);
}
return response;
})
.catch((error) => reject(error));
}
vins.forEach((vin, i) => {
taskRunner.push(task(vin, i));
});
taskRunner.onComplete().then((responses) => {
const completeMessage = `${vins.length - errorCount}/${vins.length}`;
setMessage(`Successfully deleted ${completeMessage} vehicles.`);
resolve(responses);
});
});
},
}));
return (
<div>
<p>
You are about to delete the following VINs: {vinCSV}.
</p>
</div>
);
});

View File

@@ -0,0 +1,40 @@
jest.mock("../../Contexts/UserContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../../services/vehiclesAPI");
import React from "react";
import {
render,
act,
} from "@testing-library/react";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
import DeleteVehicles from "./DeleteVehicles";
import vehiclesAPI from "../../../services/vehiclesAPI";
describe("BulkActions/DeleteVehicles", () => {
beforeAll(() => {
setToken(TEST_AUTH_OBJECT_FISKER);
});
it("makes request to delete multiple vehicles", async () => {
const api = jest.spyOn(vehiclesAPI, "deleteVehicle");
const ref = React.createRef();
render(
<StatusProvider>
<UserProvider>
<DeleteVehicles
ref={ref}
vins={["TESTVIN123456789a", "TESTVIN123456789b", "TESTVIN123456789c"]}
vinCSV=""
/>
</UserProvider>
</StatusProvider>
);
await act(async () => ref.current.submit());
expect(api).toHaveBeenCalledTimes(3);
});
});

View File

@@ -0,0 +1,74 @@
import { useState, forwardRef, useImperativeHandle } from "react";
import {
Checkbox,
FormControlLabel,
} from '@material-ui/core';
import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext";
import TaskRunner from "../../../utils/taskRunner";
import vehiclesAPI from "../../../services/vehiclesAPI";
export default forwardRef(({
vins,
vinCSV,
}, ref) => {
const { setMessage } = useStatusContext();
const { token: { idToken: { jwtToken: token } } } = useUserContext();
const [forcePush, setForcePush] = useState(false);
useImperativeHandle(ref, () => ({
async submit() {
return new Promise((resolve, reject) => {
const taskRunner = new TaskRunner(5, vins.length);
let errorCount = 0;
const task = (vin, index) => {
const progressMessage = `${index + 1}/${vins.length}`;
return async () => vehiclesAPI.updateConfig(vin, forcePush, token)
.then((response) => {
if (response.error) {
errorCount += 1;
setMessage(`${progressMessage} ${response.error}: ${response.message}`);
} else {
setMessage(`${progressMessage} Updated config for ${vin}`);
}
return response;
})
.catch((error) => reject(error));
}
vins.forEach((vin, i) => {
taskRunner.push(task(vin, i));
});
taskRunner.onComplete().then((responses) => {
const completeMessage = `${vins.length - errorCount}/${vins.length}`;
setMessage(`Successfully updated ${completeMessage} vehicles.`);
resolve(responses);
});
});
},
}));
const handleChange = () => {
setForcePush((forcePush) => !forcePush);
}
return (
<div>
<p>
You are updating the config for the following VINs: {vinCSV}.
</p>
<FormControlLabel
label="Force Push"
control={
<Checkbox
checked={forcePush}
onChange={() => handleChange()}
/>
}
/>
</div>
);
});

View File

@@ -0,0 +1,40 @@
jest.mock("../../Contexts/UserContext");
jest.mock("../../Contexts/StatusContext");
jest.mock("../../../services/vehiclesAPI");
import React from "react";
import {
render,
act,
} from "@testing-library/react";
import { UserProvider, setToken } from "../../Contexts/UserContext";
import { StatusProvider } from "../../Contexts/StatusContext";
import { TEST_AUTH_OBJECT_FISKER } from "../../../utils/testing";
import UpdateConfig from "./UpdateConfig";
import vehiclesAPI from "../../../services/vehiclesAPI";
describe("BulkActions/UpdateConfig", () => {
beforeAll(() => {
setToken(TEST_AUTH_OBJECT_FISKER);
});
it("makes request to update the config of multiple vehicles", async () => {
const api = jest.spyOn(vehiclesAPI, "updateConfig");
const ref = React.createRef();
render(
<StatusProvider>
<UserProvider>
<UpdateConfig
ref={ref}
vins={["TESTVIN123456789a", "TESTVIN123456789b", "TESTVIN123456789c"]}
vinCSV=""
/>
</UserProvider>
</StatusProvider>
);
await act(async () => ref.current.submit());
expect(api).toHaveBeenCalledTimes(3);
});
});