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:
@@ -9,7 +9,6 @@ jest.mock("../../services/vehiclesAPI");
|
||||
jest.mock("../../services/superset");
|
||||
jest.mock("../../services/suppliersAPI");
|
||||
jest.mock("../../services/issueAPI");
|
||||
jest.mock("../TransformModal");
|
||||
|
||||
import {
|
||||
act, cleanup, render,
|
||||
|
||||
@@ -6314,6 +6314,7 @@ exports[`App Route /packages authenticated 1`] = `
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
@@ -12296,17 +12297,20 @@ exports[`App Route /vehicles authenticated 1`] = `
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium Mui-disabled MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
|
||||
disabled=""
|
||||
tabindex="-1"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
Update Configs
|
||||
Add Tags
|
||||
<span
|
||||
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
@@ -12737,13 +12741,6 @@ exports[`App Route /vehicles authenticated 1`] = `
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div
|
||||
data-testid="transform-modal"
|
||||
/>
|
||||
<div
|
||||
data-testid="transform-modal"
|
||||
/>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
44
src/components/BulkActions/Modal.jsx
Normal file
44
src/components/BulkActions/Modal.jsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
} from '@material-ui/core';
|
||||
|
||||
export const Modal = ({
|
||||
open,
|
||||
close,
|
||||
submit,
|
||||
title,
|
||||
children,
|
||||
}) => {
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={close}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{children}
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
onClick={close}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={submit}
|
||||
autoFocus
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
53
src/components/BulkActions/__snapshots__/index.test.jsx.snap
Normal file
53
src/components/BulkActions/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,53 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`BulkActions Render 1`] = `
|
||||
<div>
|
||||
<div
|
||||
data-testid="mocked-statusprovider"
|
||||
>
|
||||
<div
|
||||
data-testid="mocked-userprovider"
|
||||
>
|
||||
<div
|
||||
aria-label="choose action"
|
||||
class="MuiButtonGroup-root MuiButtonGroup-contained css-zqcytf-MuiButtonGroup-root"
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
Add Tags
|
||||
<span
|
||||
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
class="MuiSvgIcon-root MuiSvgIcon-fontSizeMedium css-i4bv87-MuiSvgIcon-root"
|
||||
data-testid="ArrowDropDownIcon"
|
||||
focusable="false"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
d="m7 10 5 5 5-5z"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
43
src/components/BulkActions/actions/AddTags.jsx
Normal file
43
src/components/BulkActions/actions/AddTags.jsx
Normal 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>
|
||||
);
|
||||
});
|
||||
51
src/components/BulkActions/actions/AddTags.test.jsx
Normal file
51
src/components/BulkActions/actions/AddTags.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
99
src/components/BulkActions/actions/AddToFleet.jsx
Normal file
99
src/components/BulkActions/actions/AddToFleet.jsx
Normal 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>
|
||||
);
|
||||
});
|
||||
53
src/components/BulkActions/actions/AddToFleet.test.jsx
Normal file
53
src/components/BulkActions/actions/AddToFleet.test.jsx
Normal 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();
|
||||
});
|
||||
});
|
||||
55
src/components/BulkActions/actions/DeleteVehicles.jsx
Normal file
55
src/components/BulkActions/actions/DeleteVehicles.jsx
Normal 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>
|
||||
);
|
||||
});
|
||||
40
src/components/BulkActions/actions/DeleteVehicles.test.jsx
Normal file
40
src/components/BulkActions/actions/DeleteVehicles.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
74
src/components/BulkActions/actions/UpdateConfig.jsx
Normal file
74
src/components/BulkActions/actions/UpdateConfig.jsx
Normal 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>
|
||||
);
|
||||
});
|
||||
40
src/components/BulkActions/actions/UpdateConfig.test.jsx
Normal file
40
src/components/BulkActions/actions/UpdateConfig.test.jsx
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -1,84 +1,86 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import TransformModal from "../TransformModal";
|
||||
import { useEffect, useState, useRef, Suspense, lazy } from "react";
|
||||
import DropDownButton from "../Controls/DropDownButton";
|
||||
import { useUserContext } from "../Contexts/UserContext";
|
||||
import { useStatusContext } from "../Contexts/StatusContext";
|
||||
import useAddTags from "./useAddTags";
|
||||
import useUpdateConfig from "./useUpdateConfig";
|
||||
import { Modal } from "./Modal";
|
||||
|
||||
const transformArrayToCSV = (arr) => arr.join(", ");
|
||||
// Code-splitting individual actions
|
||||
// https://react.dev/reference/react/lazy
|
||||
const AddTags = lazy(() => import("./actions/AddTags"));
|
||||
const AddToFleet = lazy(() => import("./actions/AddToFleet"));
|
||||
const DeleteVehicles = lazy(() => import("./actions/DeleteVehicles"));
|
||||
const UpdateConfig = lazy(() => import("./actions/UpdateConfig"));
|
||||
|
||||
export default function BulkActions({
|
||||
vins = [],
|
||||
actions = [],
|
||||
}) {
|
||||
const [vinCSV, setVinCSV] = useState(transformArrayToCSV(vins));
|
||||
const [title, setTitle] = useState("Action");
|
||||
const [active, setActive] = useState(null);
|
||||
const actions = [
|
||||
{
|
||||
name: "Update Configs",
|
||||
disabled: vins.length === 0,
|
||||
trigger: () => setActive("updateConfig"),
|
||||
},
|
||||
const activeRef = useRef();
|
||||
|
||||
const filteredActions = [
|
||||
{
|
||||
id: "addTags",
|
||||
name: "Add Tags",
|
||||
disabled: vins.length === 0,
|
||||
disabled: false,
|
||||
trigger: () => setActive("addTags"),
|
||||
},
|
||||
];
|
||||
|
||||
const updateConfig = useUpdateConfig();
|
||||
const addTags = useAddTags();
|
||||
|
||||
const { setMessage } = useStatusContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
{
|
||||
id: "addToFleet",
|
||||
name: "Add To Fleet",
|
||||
disabled: false,
|
||||
trigger: () => setActive("addToFleet"),
|
||||
},
|
||||
} = useUserContext();
|
||||
{
|
||||
id: "deleteVehicles",
|
||||
name: "Delete",
|
||||
disabled: false,
|
||||
trigger: () => setActive("deleteVehicles"),
|
||||
},
|
||||
{
|
||||
id: "updateConfig",
|
||||
name: "Update Config",
|
||||
disabled: false,
|
||||
trigger: () => setActive("updateConfig"),
|
||||
}
|
||||
].filter((action) => actions.includes(action.id));
|
||||
|
||||
const handleUpdateConfig = () => {
|
||||
updateConfig.submit(vins, token)
|
||||
.then(() => {
|
||||
setMessage(`${vins.length} vehicles updated.`);
|
||||
})
|
||||
.catch((error) => {
|
||||
setMessage(error.message);
|
||||
});
|
||||
const payload = {
|
||||
vins,
|
||||
vinCSV: vins.join(", "),
|
||||
ref: activeRef
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setActive(null);
|
||||
}
|
||||
|
||||
const handleAddTags = () => {
|
||||
addTags.submit(vins, token)
|
||||
.then(() => setMessage(`Added ${addTags.data.tags.value.length} tags to ${vins.length} vehicles.`))
|
||||
.catch((error) => setMessage(error.message));
|
||||
const handleSubmit = () => {
|
||||
activeRef.current.submit();
|
||||
handleClose();
|
||||
}
|
||||
|
||||
const handleClose = () => setActive(null);
|
||||
|
||||
useEffect(() => {
|
||||
setVinCSV(transformArrayToCSV(vins));
|
||||
}, [vins]);
|
||||
setTitle(filteredActions.find((action) => active === action.id)?.name || "Action");
|
||||
}, [active, filteredActions]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DropDownButton actions={actions} payload={[vins]} />
|
||||
<TransformModal
|
||||
title="Update Config"
|
||||
body={`You are updating the config for the following VINs: ${vinCSV}.`}
|
||||
<DropDownButton actions={filteredActions} />
|
||||
<Modal
|
||||
title={title}
|
||||
open={!!active}
|
||||
close={handleClose}
|
||||
open={active === "updateConfig"}
|
||||
data={updateConfig.data}
|
||||
setData={updateConfig.setData}
|
||||
submit={handleUpdateConfig}
|
||||
/>
|
||||
<TransformModal
|
||||
title="Add Tags"
|
||||
body={`You are adding tags for the following VINs: ${vinCSV}.`}
|
||||
close={handleClose}
|
||||
open={active === "addTags"}
|
||||
data={addTags.data}
|
||||
setData={addTags.setData}
|
||||
submit={handleAddTags}
|
||||
/>
|
||||
submit={handleSubmit}
|
||||
>
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<section>
|
||||
{active === "addTags" && <AddTags {...payload} />}
|
||||
{active === "addToFleet" && <AddToFleet {...payload} />}
|
||||
{active === "deleteVehicles" && <DeleteVehicles {...payload} />}
|
||||
{active === "updateConfig" && <UpdateConfig {...payload} />}
|
||||
</section>
|
||||
</Suspense>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
77
src/components/BulkActions/index.test.jsx
Normal file
77
src/components/BulkActions/index.test.jsx
Normal file
@@ -0,0 +1,77 @@
|
||||
jest.mock("../Contexts/UserContext");
|
||||
jest.mock("../Contexts/StatusContext");
|
||||
|
||||
import React from "react";
|
||||
import {
|
||||
fireEvent,
|
||||
render,
|
||||
screen,
|
||||
waitFor,
|
||||
} from "@testing-library/react";
|
||||
import { UserProvider, setToken } from "../Contexts/UserContext";
|
||||
import { StatusProvider } from "../Contexts/StatusContext";
|
||||
import { TEST_AUTH_OBJECT_FISKER } from "../../utils/testing";
|
||||
import BulkActions from ".";
|
||||
import addSnapshotSerializer from "../../utils/snapshot";
|
||||
|
||||
describe("BulkActions", () => {
|
||||
beforeAll(() => {
|
||||
setToken(TEST_AUTH_OBJECT_FISKER);
|
||||
global.URL.createObjectURL = jest.fn();
|
||||
global.URL.revokeObjectURL = jest.fn();
|
||||
addSnapshotSerializer(expect);
|
||||
});
|
||||
|
||||
it("Render", async () => {
|
||||
const { container } = render(
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BulkActions
|
||||
actions={["addTags"]}
|
||||
vins={["TESTVIN1234567890"]}
|
||||
/>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("opens a modal", async () => {
|
||||
render(
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BulkActions
|
||||
actions={["addTags"]}
|
||||
vins={["TESTVIN1234567890"]}
|
||||
/>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
|
||||
const buttonEl = screen.getByText("Add Tags");
|
||||
fireEvent.click(buttonEl);
|
||||
const submitEl = screen.getByText("Submit");
|
||||
expect(submitEl).toBeTruthy();
|
||||
});
|
||||
|
||||
it("filters valid actions", async () => {
|
||||
render(
|
||||
<StatusProvider>
|
||||
<UserProvider>
|
||||
<BulkActions
|
||||
actions={["addTags", "someInvalidAction", "updateConfig"]}
|
||||
vins={["TESTVIN1234567890"]}
|
||||
/>
|
||||
</UserProvider>
|
||||
</StatusProvider>
|
||||
);
|
||||
|
||||
const dropdownBtn = screen.getByTestId("dropdown-button-expand");
|
||||
fireEvent.click(dropdownBtn);
|
||||
const dropdownOptions = screen.getAllByRole("menuitem");
|
||||
expect(dropdownOptions.length).toBe(2);
|
||||
});
|
||||
});
|
||||
@@ -1,22 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import vehiclesAPI from "../../services/vehiclesAPI";
|
||||
|
||||
export default function useAddTags() {
|
||||
const [tags, setTags] = useState({
|
||||
tags: {
|
||||
label: "Tags",
|
||||
type: "list.string",
|
||||
value: [],
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async (vins, token) => {
|
||||
return vehiclesAPI.addTags(vins, tags.tags.value, token);
|
||||
}
|
||||
|
||||
return {
|
||||
data: tags,
|
||||
setData: setTags,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
import { useState } from "react";
|
||||
import TaskRunner from "../../utils/taskRunner";
|
||||
import vehiclesAPI from "../../services/vehiclesAPI";
|
||||
|
||||
export default function useUpdateConfig() {
|
||||
const [config, setConfig] = useState({
|
||||
force: {
|
||||
label: "Force Push",
|
||||
type: "boolean",
|
||||
value: false,
|
||||
},
|
||||
});
|
||||
|
||||
const submit = async (vins, token) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const taskRunner = new TaskRunner(5);
|
||||
|
||||
const task = (vin, isLast) => {
|
||||
return async () => vehiclesAPI.updateConfig(vin, config.force.value, token)
|
||||
.then((response) => {
|
||||
if (isLast) {
|
||||
if (response.error) {
|
||||
reject(response);
|
||||
}
|
||||
resolve(response)
|
||||
}
|
||||
})
|
||||
.catch((error) => reject(error));
|
||||
}
|
||||
|
||||
vins.forEach((vin, index) => taskRunner.push(task(vin, index === vins.length - 1)));
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
data: config,
|
||||
setData: setConfig,
|
||||
submit,
|
||||
};
|
||||
}
|
||||
@@ -43,17 +43,20 @@ exports[`VehicleTable Render 1`] = `
|
||||
role="group"
|
||||
>
|
||||
<button
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium Mui-disabled MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
|
||||
disabled=""
|
||||
tabindex="-1"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeMedium MuiButton-containedSizeMedium MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-sghohy-MuiButtonBase-root-MuiButton-root"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
Update Configs
|
||||
Add Tags
|
||||
<span
|
||||
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||
/>
|
||||
</button>
|
||||
<button
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
@@ -482,7 +485,6 @@ exports[`VehicleTable Render 1`] = `
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,17 +7,14 @@ import { Link } from "react-router-dom";
|
||||
import { Permissions } from "../../../utils/roles";
|
||||
import { useStatusContext } from "../../Contexts/StatusContext";
|
||||
import { useUserContext } from "../../Contexts/UserContext";
|
||||
import { VehicleProvider, VehicleConsumer } from "../../Contexts/VehicleContext";
|
||||
import { VehicleProvider } 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 BulkActions from "../../BulkActions";
|
||||
import { useLocalStorage } from "../../useLocalStorage";
|
||||
import useStyles from "../../useStyles";
|
||||
import TaskRunner from "../../../utils/taskRunner";
|
||||
import GeneralConfirmation from "../../GeneralConfirmation";
|
||||
|
||||
const MainForm = () => {
|
||||
const classes = useStyles();
|
||||
@@ -25,22 +22,7 @@ const MainForm = () => {
|
||||
const [online, setOnline] = useState(false);
|
||||
const [onlineHMI, setOnlineHMI] = useState(false);
|
||||
const [selectedVins, setSelectedVins] = useState([]);
|
||||
const [config, setConfig] = useState({
|
||||
force: {
|
||||
label: "Force push",
|
||||
type: "boolean",
|
||||
value: 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 } = useStatusContext();
|
||||
const {
|
||||
token: {
|
||||
idToken: { jwtToken: token },
|
||||
@@ -70,65 +52,6 @@ const MainForm = () => {
|
||||
});
|
||||
};
|
||||
|
||||
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 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 handleDelete = async (fn) => {
|
||||
const taskRunner = new TaskRunner(5);
|
||||
const request = (vin) => {
|
||||
return async () => {
|
||||
return fn(vin, token)
|
||||
.then(() => {
|
||||
setMessage(`Deleted ${selectedVins.length} vehicles`);
|
||||
setSelectedVins([]);
|
||||
})
|
||||
.catch((error) => {
|
||||
setMessage(error.message);
|
||||
})
|
||||
}
|
||||
}
|
||||
selectedVins.forEach((vin) => taskRunner.push(request(vin)));
|
||||
};
|
||||
|
||||
const actions = [
|
||||
{
|
||||
name: "Update Configs",
|
||||
disabled: selectedVins.length === 0,
|
||||
trigger: () => setActiveModal("updateConfig"),
|
||||
},
|
||||
{
|
||||
name: "Add Tags",
|
||||
disabled: selectedVins.length === 0,
|
||||
trigger: () => setActiveModal("addTags"),
|
||||
},
|
||||
{
|
||||
name: "Delete",
|
||||
disabled: selectedVins.length === 0,
|
||||
trigger: () => setActiveModal("delete"),
|
||||
},
|
||||
];
|
||||
|
||||
const handleOnlineHMI = (event) => {
|
||||
setOnlineHMI(event.target.checked);
|
||||
};
|
||||
@@ -152,7 +75,7 @@ const MainForm = () => {
|
||||
<AddCircleIcon fontSize="large" />
|
||||
</Link>
|
||||
</RoleWrap>
|
||||
<DropDownButton actions={actions} payload={[selectedVins]} />
|
||||
<BulkActions vins={selectedVins} actions={["addTags", "addToFleet", "deleteVehicles", "updateConfig"]} />
|
||||
</Grid>
|
||||
<Grid item md={4} className={classes.textCenterAlign}>
|
||||
<SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
|
||||
@@ -190,35 +113,6 @@ const MainForm = () => {
|
||||
onSelect={handleSelect}
|
||||
onSelectAll={handleSelectAll}
|
||||
/>
|
||||
<VehicleConsumer>
|
||||
{(context) => (<>
|
||||
<TransformModal
|
||||
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)}
|
||||
/>
|
||||
<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)}
|
||||
/>
|
||||
<GeneralConfirmation
|
||||
open={activeModal === "delete"}
|
||||
close={() => setActiveModal(null)}
|
||||
title="Delete"
|
||||
message={`You are about to delete the following VINs: ${selectedVins.join(", ")}`}
|
||||
actionFunction={() => handleDelete(context.deleteVehicle)}
|
||||
/>
|
||||
</>)}
|
||||
</VehicleConsumer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ exports[`DropDownButton Render 1`] = `
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -65,6 +65,7 @@ const DropDownButton = ({ actions = [], payload = [] }) => {
|
||||
aria-label="select action"
|
||||
aria-haspopup="menu"
|
||||
onClick={handleToggle}
|
||||
data-testid="dropdown-button-expand"
|
||||
>
|
||||
<ArrowDropDownIcon />
|
||||
</Button>
|
||||
|
||||
@@ -156,7 +156,7 @@ exports[`FleetDetailsTab Render 1`] = `
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
Update Configs
|
||||
Add Tags
|
||||
<span
|
||||
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||
/>
|
||||
@@ -165,6 +165,7 @@ exports[`FleetDetailsTab Render 1`] = `
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -96,7 +96,7 @@ const MainForm = ({ name }) => {
|
||||
</Tooltip>
|
||||
</Grid>
|
||||
<Grid item md={12} className={classes.textCenterAlign}>
|
||||
<BulkActions vins={fleet.vehicles} />
|
||||
<BulkActions vins={fleet.vehicles} actions={["addTags", "updateConfig"]} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
<DeleteConfirmation message={name} open={showDeleteModal} close={() => setShowDeleteModal(false)} deleteFunction={onDelete} />
|
||||
|
||||
@@ -164,7 +164,7 @@ exports[`DetailsTab Render 1`] = `
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
Update Configs
|
||||
Add Tags
|
||||
<span
|
||||
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||
/>
|
||||
@@ -173,6 +173,7 @@ exports[`DetailsTab Render 1`] = `
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -252,7 +252,7 @@ exports[`FleetStatus Render 1`] = `
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
Update Configs
|
||||
Add Tags
|
||||
<span
|
||||
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
|
||||
/>
|
||||
@@ -261,6 +261,7 @@ exports[`FleetStatus Render 1`] = `
|
||||
aria-haspopup="menu"
|
||||
aria-label="select action"
|
||||
class="MuiButtonBase-root MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary MuiButton-root MuiButton-contained MuiButton-containedPrimary MuiButton-sizeSmall MuiButton-containedSizeSmall MuiButtonGroup-grouped MuiButtonGroup-groupedHorizontal MuiButtonGroup-groupedContained MuiButtonGroup-groupedContainedHorizontal MuiButtonGroup-groupedContainedPrimary css-11qr2p8-MuiButtonBase-root-MuiButton-root"
|
||||
data-testid="dropdown-button-expand"
|
||||
tabindex="0"
|
||||
type="button"
|
||||
>
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
const TransformModalMock = jest.fn().mockImplementation(() => {
|
||||
return <div data-testid="transform-modal" />
|
||||
});
|
||||
|
||||
export default TransformModalMock;
|
||||
@@ -1,7 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TransformModal Render 1`] = `
|
||||
<div
|
||||
aria-hidden="true"
|
||||
/>
|
||||
`;
|
||||
@@ -1,110 +0,0 @@
|
||||
import React from "react";
|
||||
import {
|
||||
Button,
|
||||
Checkbox,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
FormGroup,
|
||||
FormControlLabel,
|
||||
} from '@material-ui/core';
|
||||
import TextInputList from "../Controls/TextInputList";
|
||||
|
||||
const TransformModal = ({
|
||||
open,
|
||||
close,
|
||||
title,
|
||||
body,
|
||||
data,
|
||||
setData,
|
||||
submit
|
||||
}) => {
|
||||
const handleClick = () => {
|
||||
close();
|
||||
submit();
|
||||
};
|
||||
|
||||
const handleChange = (key, value) => {
|
||||
setData((data) => {
|
||||
const { [key]: toChange, ...rest } = data;
|
||||
switch (data[key].type) {
|
||||
case "boolean":
|
||||
toChange.value = !toChange.value;
|
||||
break;
|
||||
case "list.string":
|
||||
toChange.value = value;
|
||||
break;
|
||||
default:
|
||||
}
|
||||
return {
|
||||
[key]: toChange,
|
||||
...rest
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={open}
|
||||
onClose={close}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
{title}
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
{body && <DialogContentText>
|
||||
{body}
|
||||
</DialogContentText>}
|
||||
<FormGroup>
|
||||
{Object.entries(data).map((([key, value]) => {
|
||||
switch (value.type) {
|
||||
case "boolean":
|
||||
return (
|
||||
<FormControlLabel
|
||||
key={key}
|
||||
label={value.label}
|
||||
control={
|
||||
<Checkbox
|
||||
checked={data[key].value}
|
||||
onChange={() => handleChange(key)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)
|
||||
case "list.string":
|
||||
return (
|
||||
<TextInputList
|
||||
key={key}
|
||||
label={value.label}
|
||||
onChange={(list) => handleChange(key, list)}
|
||||
/>
|
||||
)
|
||||
default:
|
||||
return <></>;
|
||||
}
|
||||
}))}
|
||||
</FormGroup>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button
|
||||
label="Test"
|
||||
onClick={close}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="secondary"
|
||||
onClick={handleClick}
|
||||
autoFocus
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default TransformModal;
|
||||
@@ -1,56 +0,0 @@
|
||||
import React from "react";
|
||||
import { render, waitFor } from "@testing-library/react";
|
||||
|
||||
import TransformModal from ".";
|
||||
import addSnapshotSerializer from "../../utils/snapshot";
|
||||
|
||||
const data = {
|
||||
test: {
|
||||
label: "Test field",
|
||||
value: false,
|
||||
type: "boolean",
|
||||
},
|
||||
}
|
||||
|
||||
describe("TransformModal", () => {
|
||||
beforeAll(() => {
|
||||
addSnapshotSerializer(expect);
|
||||
});
|
||||
|
||||
it("Render", async () => {
|
||||
|
||||
const { container } = render(
|
||||
<TransformModal
|
||||
open={true}
|
||||
close={() => {}}
|
||||
title="Title"
|
||||
body="Body"
|
||||
data={data}
|
||||
setData={() => {}}
|
||||
submit={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("properly renders a checkbox for a boolean", async () => {
|
||||
const { getByText } = render(
|
||||
<TransformModal
|
||||
open={true}
|
||||
close={() => {}}
|
||||
title="Title"
|
||||
body="Body"
|
||||
data={data}
|
||||
setData={() => {}}
|
||||
submit={() => {}}
|
||||
/>
|
||||
);
|
||||
await waitFor(() => {
|
||||
/* render */
|
||||
});
|
||||
expect(getByText("Test field")).toBeTruthy();
|
||||
});
|
||||
});
|
||||
@@ -62,9 +62,9 @@ const fleetsAPI = {
|
||||
getFleetVehicles: async () => {
|
||||
return { data: vehicles };
|
||||
},
|
||||
addFleetVehicles: async (_name, vehicle) => {
|
||||
vehicles.push(...vehicle.vins);
|
||||
return vehicle;
|
||||
addFleetVehicles: async (_name, payload) => {
|
||||
payload.vins && vehicles.push(...payload.vins);
|
||||
return payload;
|
||||
},
|
||||
deleteFleetVehicle: async (_name, vehicle) => {
|
||||
const index = vehicles.findIndex(element => element === vehicle.vin);
|
||||
|
||||
@@ -98,6 +98,12 @@ const trexLogs = {
|
||||
}
|
||||
|
||||
const vehiclesAPI = {
|
||||
addTags: async (vins, tags) => {
|
||||
return {
|
||||
vins,
|
||||
tags,
|
||||
};
|
||||
},
|
||||
addVehicle: async (vehicle) => {
|
||||
data.push(vehicle);
|
||||
return vehicle;
|
||||
@@ -161,6 +167,9 @@ const vehiclesAPI = {
|
||||
if (index >= 0) data[index] = vehicle;
|
||||
return vehicle;
|
||||
},
|
||||
updateConfig: async (vin, vehicle) => {
|
||||
return { message: "Sent" };
|
||||
},
|
||||
getCANSignals: async (vin, vehicle) => {
|
||||
return signals;
|
||||
},
|
||||
|
||||
@@ -28,13 +28,14 @@ const fleetsAPI = {
|
||||
.then(fetchRespHandler)
|
||||
.catch(errorHandler),
|
||||
|
||||
getFleets: async (search, token) =>
|
||||
getFleets: async (search, token, controller) =>
|
||||
fetch(addQueryParams(`${API_ENDPOINT}/fleets`, search), {
|
||||
method: "GET",
|
||||
headers: Object.assign(
|
||||
{ "Content-Type": "application/json" },
|
||||
getAuthHeaderOptions(token)
|
||||
),
|
||||
signal: controller?.signal,
|
||||
})
|
||||
.then(fetchRespHandler)
|
||||
.catch(errorHandler),
|
||||
|
||||
@@ -3,3 +3,7 @@
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import "@testing-library/jest-dom";
|
||||
|
||||
if (typeof window.URL.createObjectURL === 'undefined') {
|
||||
window.URL.createObjectURL = jest.fn();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user