Merge branch 'release/0.9.0'

This commit is contained in:
jwu-fisker
2023-07-10 15:51:54 -07:00
35 changed files with 809 additions and 441 deletions

View File

@@ -9,7 +9,6 @@ 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,

View File

@@ -6314,6 +6314,7 @@ exports[`App Route /packages authenticated 1`] = `
aria-haspopup="menu" aria-haspopup="menu"
aria-label="select action" 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" 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" tabindex="0"
type="button" type="button"
> >
@@ -8205,6 +8206,57 @@ exports[`App Route /tools/certificates/add authenticated 1`] = `
Aftersales Aftersales
</span> </span>
</label> </label>
<label
class="MuiFormControlLabel-root"
>
<span
aria-disabled="false"
class="MuiButtonBase-root MuiIconButton-root PrivateSwitchBase-root-0 MuiRadio-root MuiRadio-colorSecondary MuiIconButton-colorSecondary"
>
<span
class="MuiIconButton-label"
>
<input
class="PrivateSwitchBase-input-0"
name="cert-type"
type="radio"
value="AftersalesEU"
/>
<div
class="PrivateRadioButtonIcon-root-0"
>
<svg
aria-hidden="true"
class="MuiSvgIcon-root"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"
/>
</svg>
<svg
aria-hidden="true"
class="MuiSvgIcon-root PrivateRadioButtonIcon-layer-0"
focusable="false"
viewBox="0 0 24 24"
>
<path
d="M8.465 8.465C9.37 7.56 10.62 7 12 7C14.76 7 17 9.24 17 12C17 13.38 16.44 14.63 15.535 15.535C14.63 16.44 13.38 17 12 17C9.24 17 7 14.76 7 12C7 10.62 7.56 9.37 8.465 8.465Z"
/>
</svg>
</div>
</span>
<span
class="MuiTouchRipple-root"
/>
</span>
<span
class="MuiTypography-root MuiFormControlLabel-label MuiTypography-body1"
>
Aftersales EU
</span>
</label>
</div> </div>
<button <button
class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth" class="MuiButtonBase-root MuiButton-root MuiButton-contained makeStyles-submit-0 MuiButton-containedPrimary MuiButton-fullWidth"
@@ -12296,17 +12348,20 @@ exports[`App Route /vehicles authenticated 1`] = `
role="group" role="group"
> >
<button <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" 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"
disabled="" tabindex="0"
tabindex="-1"
type="button" type="button"
> >
Update Configs Add Tags
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button> </button>
<button <button
aria-haspopup="menu" aria-haspopup="menu"
aria-label="select action" 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" 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" tabindex="0"
type="button" type="button"
> >
@@ -12737,13 +12792,6 @@ 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> </div>
</main> </main>

View 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>
)
}

View 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>
`;

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);
});
});

View File

@@ -1,84 +1,86 @@
import { useEffect, useState } from "react"; import { useEffect, useState, useRef, Suspense, lazy } from "react";
import TransformModal from "../TransformModal";
import DropDownButton from "../Controls/DropDownButton"; import DropDownButton from "../Controls/DropDownButton";
import { useUserContext } from "../Contexts/UserContext"; import { Modal } from "./Modal";
import { useStatusContext } from "../Contexts/StatusContext";
import useAddTags from "./useAddTags";
import useUpdateConfig from "./useUpdateConfig";
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({ export default function BulkActions({
vins = [], vins = [],
actions = [],
}) { }) {
const [vinCSV, setVinCSV] = useState(transformArrayToCSV(vins)); const [title, setTitle] = useState("Action");
const [active, setActive] = useState(null); const [active, setActive] = useState(null);
const actions = [ const activeRef = useRef();
{
name: "Update Configs", const filteredActions = [
disabled: vins.length === 0,
trigger: () => setActive("updateConfig"),
},
{ {
id: "addTags",
name: "Add Tags", name: "Add Tags",
disabled: vins.length === 0, disabled: false,
trigger: () => setActive("addTags"), trigger: () => setActive("addTags"),
}, },
]; {
id: "addToFleet",
const updateConfig = useUpdateConfig(); name: "Add To Fleet",
const addTags = useAddTags(); disabled: false,
trigger: () => setActive("addToFleet"),
const { setMessage } = useStatusContext();
const {
token: {
idToken: { jwtToken: token },
}, },
} = 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 = () => { const payload = {
updateConfig.submit(vins, token) vins,
.then(() => { vinCSV: vins.join(", "),
setMessage(`${vins.length} vehicles updated.`); ref: activeRef
}) };
.catch((error) => {
setMessage(error.message); const handleClose = () => {
}); setActive(null);
} }
const handleAddTags = () => { const handleSubmit = () => {
addTags.submit(vins, token) activeRef.current.submit();
.then(() => setMessage(`Added ${addTags.data.tags.value.length} tags to ${vins.length} vehicles.`)) handleClose();
.catch((error) => setMessage(error.message));
} }
const handleClose = () => setActive(null);
useEffect(() => { useEffect(() => {
setVinCSV(transformArrayToCSV(vins)); setTitle(filteredActions.find((action) => active === action.id)?.name || "Action");
}, [vins]); }, [active, filteredActions]);
return ( return (
<> <>
<DropDownButton actions={actions} payload={[vins]} /> <DropDownButton actions={filteredActions} />
<TransformModal <Modal
title="Update Config" title={title}
body={`You are updating the config for the following VINs: ${vinCSV}.`} open={!!active}
close={handleClose} close={handleClose}
open={active === "updateConfig"} submit={handleSubmit}
data={updateConfig.data} >
setData={updateConfig.setData} <Suspense fallback={<div>Loading...</div>}>
submit={handleUpdateConfig} <section>
/> {active === "addTags" && <AddTags {...payload} />}
<TransformModal {active === "addToFleet" && <AddToFleet {...payload} />}
title="Add Tags" {active === "deleteVehicles" && <DeleteVehicles {...payload} />}
body={`You are adding tags for the following VINs: ${vinCSV}.`} {active === "updateConfig" && <UpdateConfig {...payload} />}
close={handleClose} </section>
open={active === "addTags"} </Suspense>
data={addTags.data} </Modal>
setData={addTags.setData}
submit={handleAddTags}
/>
</> </>
); )
} }

View 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);
});
});

View File

@@ -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,
};
}

View File

@@ -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,
};
}

View File

@@ -43,17 +43,20 @@ exports[`VehicleTable Render 1`] = `
role="group" role="group"
> >
<button <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" 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"
disabled="" tabindex="0"
tabindex="-1"
type="button" type="button"
> >
Update Configs Add Tags
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button> </button>
<button <button
aria-haspopup="menu" aria-haspopup="menu"
aria-label="select action" 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" 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" tabindex="0"
type="button" type="button"
> >
@@ -482,7 +485,6 @@ exports[`VehicleTable Render 1`] = `
</tfoot> </tfoot>
</table> </table>
</div> </div>
<div />
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,17 +7,14 @@ import { Link } from "react-router-dom";
import { Permissions } from "../../../utils/roles"; import { Permissions } from "../../../utils/roles";
import { useStatusContext } from "../../Contexts/StatusContext"; import { useStatusContext } from "../../Contexts/StatusContext";
import { useUserContext } from "../../Contexts/UserContext"; import { useUserContext } from "../../Contexts/UserContext";
import { VehicleProvider, VehicleConsumer } from "../../Contexts/VehicleContext"; import { VehicleProvider } from "../../Contexts/VehicleContext";
import CarSelectionTable from "../../Controls/CarSelectionTable"; import CarSelectionTable from "../../Controls/CarSelectionTable";
import OptionsDropdown from "../../Controls/OptionsDropdown"; import OptionsDropdown from "../../Controls/OptionsDropdown";
import { RoleWrap } from "../../Controls/RoleWrap"; import { RoleWrap } from "../../Controls/RoleWrap";
import SearchField from "../../Controls/SearchField"; import SearchField from "../../Controls/SearchField";
import DropDownButton from "../../Controls/DropDownButton"; import BulkActions from "../../BulkActions";
import TransformModal from "../../TransformModal";
import { useLocalStorage } from "../../useLocalStorage"; import { useLocalStorage } from "../../useLocalStorage";
import useStyles from "../../useStyles"; import useStyles from "../../useStyles";
import TaskRunner from "../../../utils/taskRunner";
import GeneralConfirmation from "../../GeneralConfirmation";
const MainForm = () => { const MainForm = () => {
const classes = useStyles(); const classes = useStyles();
@@ -25,22 +22,7 @@ const MainForm = () => {
const [online, setOnline] = useState(false); const [online, setOnline] = useState(false);
const [onlineHMI, setOnlineHMI] = useState(false); const [onlineHMI, setOnlineHMI] = useState(false);
const [selectedVins, setSelectedVins] = useState([]); const [selectedVins, setSelectedVins] = useState([]);
const [config, setConfig] = useState({ const { setTitle, setSitePath } = useStatusContext();
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 { const {
token: { token: {
idToken: { jwtToken: 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) => { const handleOnlineHMI = (event) => {
setOnlineHMI(event.target.checked); setOnlineHMI(event.target.checked);
}; };
@@ -152,7 +75,7 @@ const MainForm = () => {
<AddCircleIcon fontSize="large" /> <AddCircleIcon fontSize="large" />
</Link> </Link>
</RoleWrap> </RoleWrap>
<DropDownButton actions={actions} payload={[selectedVins]} /> <BulkActions vins={selectedVins} actions={["addTags", "addToFleet", "deleteVehicles", "updateConfig"]} />
</Grid> </Grid>
<Grid item md={4} className={classes.textCenterAlign}> <Grid item md={4} className={classes.textCenterAlign}>
<SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} /> <SearchField classes={classes} onSearch={handleSearch} savedSearchValue={search} />
@@ -190,35 +113,6 @@ const MainForm = () => {
onSelect={handleSelect} onSelect={handleSelect}
onSelectAll={handleSelectAll} 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> </div>
); );
}; };

View File

@@ -45,10 +45,19 @@ const CreateForm = ({ onCreate, busy }) => {
event.preventDefault(); event.preventDefault();
if (onCreate) if (onCreate)
onCreate({ if (certType === CertTypes.AftersalesEU) {
common_name: commonName, onCreate({
type: certType, common_name: commonName,
}); type: CertTypes.Aftersales,
is_eu: true,
});
} else {
onCreate({
common_name: commonName,
type: certType,
is_eu: false,
});
}
}; };
const onCertTypeChange = (event) => { const onCertTypeChange = (event) => {

View File

@@ -21,6 +21,7 @@ exports[`DropDownButton Render 1`] = `
aria-haspopup="menu" aria-haspopup="menu"
aria-label="select action" 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" 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" tabindex="0"
type="button" type="button"
> >

View File

@@ -65,6 +65,7 @@ const DropDownButton = ({ actions = [], payload = [] }) => {
aria-label="select action" aria-label="select action"
aria-haspopup="menu" aria-haspopup="menu"
onClick={handleToggle} onClick={handleToggle}
data-testid="dropdown-button-expand"
> >
<ArrowDropDownIcon /> <ArrowDropDownIcon />
</Button> </Button>

View File

@@ -156,7 +156,7 @@ exports[`FleetDetailsTab Render 1`] = `
tabindex="0" tabindex="0"
type="button" type="button"
> >
Update Configs Add Tags
<span <span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root" class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/> />
@@ -165,6 +165,7 @@ exports[`FleetDetailsTab Render 1`] = `
aria-haspopup="menu" aria-haspopup="menu"
aria-label="select action" 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" 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" tabindex="0"
type="button" type="button"
> >

View File

@@ -96,7 +96,7 @@ const MainForm = ({ name }) => {
</Tooltip> </Tooltip>
</Grid> </Grid>
<Grid item md={12} className={classes.textCenterAlign}> <Grid item md={12} className={classes.textCenterAlign}>
<BulkActions vins={fleet.vehicles} /> <BulkActions vins={fleet.vehicles} actions={["addTags", "updateConfig"]} />
</Grid> </Grid>
</Grid> </Grid>
<DeleteConfirmation message={name} open={showDeleteModal} close={() => setShowDeleteModal(false)} deleteFunction={onDelete} /> <DeleteConfirmation message={name} open={showDeleteModal} close={() => setShowDeleteModal(false)} deleteFunction={onDelete} />

View File

@@ -164,7 +164,7 @@ exports[`DetailsTab Render 1`] = `
tabindex="0" tabindex="0"
type="button" type="button"
> >
Update Configs Add Tags
<span <span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root" class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/> />
@@ -173,6 +173,7 @@ exports[`DetailsTab Render 1`] = `
aria-haspopup="menu" aria-haspopup="menu"
aria-label="select action" 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" 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" tabindex="0"
type="button" type="button"
> >

View File

@@ -252,7 +252,7 @@ exports[`FleetStatus Render 1`] = `
tabindex="0" tabindex="0"
type="button" type="button"
> >
Update Configs Add Tags
<span <span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root" class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/> />
@@ -261,6 +261,7 @@ exports[`FleetStatus Render 1`] = `
aria-haspopup="menu" aria-haspopup="menu"
aria-label="select action" 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" 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" tabindex="0"
type="button" type="button"
> >

View File

@@ -1,5 +0,0 @@
const TransformModalMock = jest.fn().mockImplementation(() => {
return <div data-testid="transform-modal" />
});
export default TransformModalMock;

View File

@@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`TransformModal Render 1`] = `
<div
aria-hidden="true"
/>
`;

View File

@@ -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;

View File

@@ -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();
});
});

View File

@@ -62,9 +62,9 @@ const fleetsAPI = {
getFleetVehicles: async () => { getFleetVehicles: async () => {
return { data: vehicles }; return { data: vehicles };
}, },
addFleetVehicles: async (_name, vehicle) => { addFleetVehicles: async (_name, payload) => {
vehicles.push(...vehicle.vins); payload.vins && vehicles.push(...payload.vins);
return vehicle; return payload;
}, },
deleteFleetVehicle: async (_name, vehicle) => { deleteFleetVehicle: async (_name, vehicle) => {
const index = vehicles.findIndex(element => element === vehicle.vin); const index = vehicles.findIndex(element => element === vehicle.vin);

View File

@@ -98,6 +98,12 @@ const trexLogs = {
} }
const vehiclesAPI = { const vehiclesAPI = {
addTags: async (vins, tags) => {
return {
vins,
tags,
};
},
addVehicle: async (vehicle) => { addVehicle: async (vehicle) => {
data.push(vehicle); data.push(vehicle);
return vehicle; return vehicle;
@@ -161,6 +167,9 @@ const vehiclesAPI = {
if (index >= 0) data[index] = vehicle; if (index >= 0) data[index] = vehicle;
return vehicle; return vehicle;
}, },
updateConfig: async (vin, vehicle) => {
return { message: "Sent" };
},
getCANSignals: async (vin, vehicle) => { getCANSignals: async (vin, vehicle) => {
return signals; return signals;
}, },

View File

@@ -1,5 +1,5 @@
import { import {
addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions addQueryParams, errorHandler, fetchRespHandler, getAuthHeaderOptions
} from "../utils/http"; } from "../utils/http";
const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL; const API_ENDPOINT = process.env.REACT_APP_OTA_SERVICE_URL;
@@ -28,13 +28,14 @@ const fleetsAPI = {
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler), .catch(errorHandler),
getFleets: async (search, token) => getFleets: async (search, token, controller) =>
fetch(addQueryParams(`${API_ENDPOINT}/fleets`, search), { fetch(addQueryParams(`${API_ENDPOINT}/fleets`, search), {
method: "GET", method: "GET",
headers: Object.assign( headers: Object.assign(
{ "Content-Type": "application/json" }, { "Content-Type": "application/json" },
getAuthHeaderOptions(token) getAuthHeaderOptions(token)
), ),
signal: controller?.signal,
}) })
.then(fetchRespHandler) .then(fetchRespHandler)
.catch(errorHandler), .catch(errorHandler),

View File

@@ -3,3 +3,7 @@
// expect(element).toHaveTextContent(/react/i) // expect(element).toHaveTextContent(/react/i)
// learn more: https://github.com/testing-library/jest-dom // learn more: https://github.com/testing-library/jest-dom
import "@testing-library/jest-dom"; import "@testing-library/jest-dom";
if (typeof window.URL.createObjectURL === 'undefined') {
window.URL.createObjectURL = jest.fn();
}

View File

@@ -2,6 +2,7 @@ export const CertTypes = {
TBOX: "TBOX", TBOX: "TBOX",
Charging: "Charging", Charging: "Charging",
Aftersales: "Aftersales", Aftersales: "Aftersales",
AftersalesEU: "AftersalesEU",
}; };
export const CertTypeData = [ export const CertTypeData = [
@@ -20,4 +21,9 @@ export const CertTypeData = [
label: "Aftersales", label: "Aftersales",
inputlabel: "Service Tool ID", inputlabel: "Service Tool ID",
}, },
{
value: CertTypes.AftersalesEU,
label: "Aftersales EU",
inputlabel: "Service Tool ID",
},
]; ];