diff --git a/src/components/App/__snapshots__/App.test.js.snap b/src/components/App/__snapshots__/App.test.js.snap
index c6a7b02..7689412 100644
--- a/src/components/App/__snapshots__/App.test.js.snap
+++ b/src/components/App/__snapshots__/App.test.js.snap
@@ -5301,14 +5301,63 @@ exports[`App Route /package-status authenticated 1`] = `
-
- Show Details
-
+
@@ -5319,39 +5368,160 @@ exports[`App Route /package-status authenticated 1`] = `
class="MuiTableRow-root MuiTableRow-head"
>
|
+
+
+
+
+
+
+
+ |
+
- ID
+
+ ID
+
+ sorted ascending
+
+
+
|
- Vehicle
+
+ Vehicle
+
+
|
- Status
+
+ Status
+
+
|
- Created
+
+ Created
+
+
|
- Updated
+
+ Updated
+
+
|
- |
+
+
+
+
+
+
+
+
+ |
@@ -5390,30 +5592,42 @@ exports[`App Route /package-status authenticated 1`] = `
>
7/12/2021 6:22:13 PM
|
-
-
-
-
- |
+ |
+
+
+
+
+
+
+
+ |
@@ -5444,30 +5658,42 @@ exports[`App Route /package-status authenticated 1`] = `
>
7/12/2021 6:22:13 PM
|
-
-
-
-
- |
+ |
+
+
+
+
+
+
+
+ |
@@ -5498,26 +5724,6 @@ exports[`App Route /package-status authenticated 1`] = `
>
7/12/2021 6:22:13 PM
|
-
-
-
-
- |
{
const { manifest_id } = useParams();
const classes = useStyles();
const [pageSize, setPageSize] = useLocalStorage(PAGE_SIZE, 10);
const [pageIndex, setPageIndex] = useState(0);
+ const [orderBy, setOrderBy] = useState("id");
+ const [order, setOrder] = useState("asc");
+ const [ids, setIds] = useState([]);
const { getManifests, manifests } = useManifestsContext();
const {
- cancelUpdate,
getCarUpdates,
carUpdates,
totalCarUpdates,
@@ -54,10 +78,35 @@ const MainForm = () => {
token: {
idToken: { jwtToken: token },
},
- groups,
- providers,
} = useUserContext();
+ const handleSelectAll = () => {
+ setIds((ids) => ids.length === 0
+ ? carUpdates.map((carUpdate) => carUpdate.id)
+ : []);
+ }
+
+ const handleSelect = (newId, selected) => {
+ if (selected) {
+ setIds((ids) => ids.filter((id) => id !== newId));
+ } else {
+ setIds((ids) => [...ids, newId]);
+ }
+ }
+
+ const handleSort = (_event, property) => {
+ if (property === orderBy) {
+ if (order === "asc") {
+ setOrder("desc");
+ } else {
+ setOrder("asc");
+ }
+ } else {
+ setOrderBy(property);
+ setOrder("asc");
+ }
+ };
+
useEffect(() => {
(async () => {
try {
@@ -100,6 +149,7 @@ const MainForm = () => {
manifest_id,
limit: pageSize,
offset: pageSize * pageIndex,
+ order: `${orderBy} ${order}`,
},
token
);
@@ -109,7 +159,7 @@ const MainForm = () => {
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [pageIndex, pageSize, token]);
+ }, [pageIndex, pageSize, token, order, orderBy]);
useEffect(() => {
try {
@@ -134,63 +184,58 @@ const MainForm = () => {
setPageIndex(0);
};
- const sendCancel = async ({ id, vin }) => {
- try {
- await cancelUpdate(id, token);
- setMessage(`Sent cancel for ${vin}`);
- } catch (e) {
- setMessage(e.message);
- }
- };
-
return (
+
+
+
+
+
+
+
+
-
-
- ID
- Vehicle
- Status
- Created
- Updated
-
-
-
+
- {carUpdates.map((row) => (
-
- {row.id}
-
- {row.vin}
-
-
- {row.status}
- {row.progress > -1 && (
-
- )}
-
-
- {LocalDateTimeString(row.created)}
-
-
- {LocalDateTimeString(row.updated)}
-
-
- No action>}
- >
-
- sendCancel(row)}>
-
-
-
-
-
-
- ))}
+ {carUpdates.map((row) => {
+ const isSelected = ids.indexOf(row.id) !== -1;
+ return (
+
+
+ handleSelect(row.id, isSelected)}
+ />
+
+ {row.id}
+
+ {row.vin}
+
+
+ {row.status}
+ {row.progress > -1 && (
+
+ )}
+
+
+ {LocalDateTimeString(row.created)}
+
+
+ {LocalDateTimeString(row.updated)}
+
+
+ )
+ })}
@@ -217,7 +262,6 @@ const MainForm = () => {
const ManifestStatus = () => (
-
diff --git a/src/components/Manifest/Toolbar/__snapshots__/index.test.jsx.snap b/src/components/Manifest/Toolbar/__snapshots__/index.test.jsx.snap
new file mode 100644
index 0000000..eec1c36
--- /dev/null
+++ b/src/components/Manifest/Toolbar/__snapshots__/index.test.jsx.snap
@@ -0,0 +1,53 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Toolbar Render 1`] = `
+
+`;
diff --git a/src/components/Manifest/Toolbar/actions/Cancel.jsx b/src/components/Manifest/Toolbar/actions/Cancel.jsx
new file mode 100644
index 0000000..2a815dc
--- /dev/null
+++ b/src/components/Manifest/Toolbar/actions/Cancel.jsx
@@ -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 updatesAPI from "../../../../services/updatesAPI";
+
+export default forwardRef(({
+ ids,
+ idCSV,
+}, ref) => {
+ const { setMessage } = useStatusContext();
+ const { token: { idToken: { jwtToken: token } } } = useUserContext();
+
+ useImperativeHandle(ref, () => ({
+ async submit() {
+ return new Promise((resolve, reject) => {
+ const taskRunner = new TaskRunner(5, ids.length);
+ let errorCount = 0;
+
+ const task = (id, index) => {
+ const progressMessage = `${index + 1}/${ids.length}`;
+ return async () => updatesAPI.cancelCarUpdate(id, token)
+ .then((response) => {
+ if (response.error) {
+ errorCount += 1;
+ setMessage(`${progressMessage} ${response.error}: ${response.message}`);
+ } else {
+ setMessage(`${progressMessage} Canceled update ${id}`);
+ }
+ return response;
+ })
+ .catch((error) => reject(error));
+ }
+
+ ids.forEach((id, i) => {
+ taskRunner.push(task(id, i));
+ });
+
+ taskRunner.onComplete().then((responses) => {
+ const completeMessage = `${ids.length - errorCount}/${ids.length}`;
+ setMessage(`Successfully canceled ${completeMessage} updates.`);
+ resolve(responses);
+ });
+ });
+ },
+ }));
+
+ return (
+
+
+ You are canceling the following updates: {idCSV}.
+
+
+ );
+});
\ No newline at end of file
diff --git a/src/components/Manifest/Toolbar/actions/Cancel.test.jsx b/src/components/Manifest/Toolbar/actions/Cancel.test.jsx
new file mode 100644
index 0000000..1fa05b3
--- /dev/null
+++ b/src/components/Manifest/Toolbar/actions/Cancel.test.jsx
@@ -0,0 +1,40 @@
+jest.mock("../../../Contexts/UserContext");
+jest.mock("../../../Contexts/StatusContext");
+jest.mock("../../../../services/updatesAPI");
+
+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 Cancel from "./Cancel";
+import updatesAPI from "../../../../services/updatesAPI";
+
+describe("Manifest/Cancel", () => {
+ beforeAll(() => {
+ setToken(TEST_AUTH_OBJECT_FISKER);
+ });
+
+ it("makes request to cancel an update", async () => {
+ const api = jest.spyOn(updatesAPI, "cancelCarUpdate");
+ const ref = React.createRef();
+
+ render(
+
+
+
+
+
+ );
+
+ await act(async () => ref.current.submit());
+ expect(api).toHaveBeenCalledTimes(3);
+ });
+});
\ No newline at end of file
diff --git a/src/components/Manifest/Toolbar/index.jsx b/src/components/Manifest/Toolbar/index.jsx
new file mode 100644
index 0000000..b98d892
--- /dev/null
+++ b/src/components/Manifest/Toolbar/index.jsx
@@ -0,0 +1,71 @@
+import { useEffect, useState, useRef, Suspense, lazy } from "react";
+import DropDownButton from "../../Controls/DropDownButton";
+import { Modal } from "../../BulkActions/Modal";
+import { useUserContext } from "../../Contexts/UserContext";
+import { Permissions, hasRole } from "../../../utils/roles";
+
+// Code-splitting individual actions
+// https://react.dev/reference/react/lazy
+const Cancel = lazy(() => import("./actions/Cancel"));
+
+export default function Toolbar({
+ ids = [],
+ actions = [],
+}) {
+ const [title, setTitle] = useState("Action");
+ const [open, setOpen] = useState(false);
+ const [active, setActive] = useState(null);
+ const activeRef = useRef();
+ const { groups, providers } = useUserContext();
+
+ const hasAccess = hasRole(groups, Permissions.FiskerMagnaCreate, providers);
+
+ const filteredActions = [
+ {
+ id: "cancel",
+ name: "Cancel Updates",
+ disabled: !hasAccess || ids.length <= 0,
+ trigger: () => {
+ setOpen(true);
+ setActive("cancel");
+ },
+ },
+ ].filter((action) => actions.includes(action.id));
+
+ const payload = {
+ ids,
+ idCSV: ids.join(", "),
+ ref: activeRef
+ };
+
+ const handleClose = () => {
+ setOpen(false).then(() => setActive(null));
+ }
+
+ const handleSubmit = () => {
+ activeRef.current.submit();
+ handleClose();
+ }
+
+ useEffect(() => {
+ setTitle(filteredActions.find((action) => active === action.id)?.name || "Action");
+ }, [active, filteredActions]);
+
+ return (
+ <>
+
+
+ Loading...}>
+
+ {active === "cancel" && }
+
+
+
+ >
+ )
+}
\ No newline at end of file
diff --git a/src/components/Manifest/Toolbar/index.test.jsx b/src/components/Manifest/Toolbar/index.test.jsx
new file mode 100644
index 0000000..1fe9bbd
--- /dev/null
+++ b/src/components/Manifest/Toolbar/index.test.jsx
@@ -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 Toolbar from ".";
+import addSnapshotSerializer from "../../../utils/snapshot";
+
+describe("Toolbar", () => {
+ beforeAll(() => {
+ setToken(TEST_AUTH_OBJECT_FISKER);
+ global.URL.createObjectURL = jest.fn();
+ global.URL.revokeObjectURL = jest.fn();
+ addSnapshotSerializer(expect);
+ });
+
+ it("Render", async () => {
+ const { container } = render(
+
+
+
+
+
+ );
+ await waitFor(() => {
+ /* render */
+ });
+ expect(container).toMatchSnapshot();
+ });
+
+ it("opens a modal", async () => {
+ render(
+
+
+
+
+
+ );
+
+ const buttonEl = screen.getByText("Cancel Updates");
+ fireEvent.click(buttonEl);
+ const submitEl = screen.getByText("Submit");
+ expect(submitEl).toBeTruthy();
+ });
+
+ it("filters valid actions", async () => {
+ render(
+
+
+
+
+
+ );
+
+ const dropdownBtn = screen.getByTestId("dropdown-button-expand");
+ fireEvent.click(dropdownBtn);
+ const dropdownOptions = screen.getAllByRole("menuitem");
+ expect(dropdownOptions.length).toBe(1);
+ });
+});
\ No newline at end of file