Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions hackathon_site/dashboard/frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import Orders from "pages/Orders/Orders";
import Teams from "pages/Teams/Teams";
import Reports from "pages/Reports/Reports";
import Inventory from "pages/Inventory/Inventory";
import Threedprinting from "pages/Threedprinting/Threedprinting";
// import ThreeDPrinting from "pages/ThreeDPrinting/ThreeDPrinting"
import Cart from "pages/Cart/Cart";
import IncidentForm from "pages/IncidentForm/IncidentForm";
import NotFound from "pages/NotFound/NotFound";
Expand Down Expand Up @@ -84,6 +86,11 @@ const UnconnectedApp = () => {
path="/inventory"
component={withUserCheck("both", Inventory)}
/>
<Route
exact
path="/threedprinting"
component={withUserCheck("both", Threedprinting)}
/>
<Route
exact
path="/cart"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { connect, useSelector } from "react-redux";
// Images and logos
import DashboardIcon from "@material-ui/icons/Dashboard";
import LocalMallIcon from "@material-ui/icons/LocalMall";
import CategoryIcon from "@material-ui/icons/Category";
import AccountBalanceWalletIcon from "@material-ui/icons/AccountBalanceWallet";
import ListAlt from "@material-ui/icons/ListAlt";
import AccountBoxIcon from "@material-ui/icons/AccountBox";
Expand Down Expand Up @@ -118,6 +119,23 @@ export const UnconnectedNavbar = ({ logout, pathname }: NavBarProps) => {
</Button>
</Link>
)}
{isParticipantOrAdmin && (
<Link to={"/threedprinting"}>
<Button
className={
pathname === "/threedprinting"
? styles.navActive
: styles.navBtn
}
aria-label="threedprinting"
startIcon={
<CategoryIcon fill="currentColor" width="20px" />
}
>
3D Printing Services
</Button>
</Link>
)}
{isParticipant && (
<Link to={"/cart"}>
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import {
import {
categorySelectors,
isLoadingSelector as isCategoriesLoadingSelector,
categorySelectorsFiltered,
selectNonThreeDCategoryIds,
} from "slices/hardware/categorySlice";
import styles from "components/sharedStyles/Filter.module.scss";
type OrderByOptions = {
Expand Down Expand Up @@ -95,7 +97,7 @@ interface InventoryFilterValues {
}

export const InventoryFilter = ({ handleReset, handleSubmit }: FormikValues) => {
const categories = useSelector(categorySelectors.selectAll);
const categories = useSelector(categorySelectorsFiltered);
const isCategoriesLoading = useSelector(isCategoriesLoadingSelector);
const isHardwareLoading = useSelector(isHardwareLoadingSelector);
return (
Expand Down Expand Up @@ -169,12 +171,14 @@ export const InventoryFilter = ({ handleReset, handleSubmit }: FormikValues) =>

export const EnhancedInventoryFilter = () => {
const dispatch = useDispatch();

const nonThreeDIds = useSelector(selectNonThreeDCategoryIds);
const onSubmit = ({ ordering, in_stock, categories }: InventoryFilterValues) => {
const selectedIds = categories.map((id) => parseInt(id, 10));
const filters: HardwareFilters = {
ordering,
in_stock: in_stock || undefined, // If false, it will be cleared below
category_ids: categories.map((id) => parseInt(id, 10)),
// category_ids: categories.map((id) => parseInt(id, 10)),
category_ids: selectedIds.length > 0 ? selectedIds : nonThreeDIds,
};

dispatch(setFilters(filters));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@import "./../../../assets/abstracts/variables";
@import "./../../../assets/abstracts/mixins";

.Item {
@include flexPosition($dir: column);
cursor: pointer;

&:hover {
background-color: color(light2);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import React from "react";

import {
render,
waitFor,
promiseResolveWithDelay,
makeMockApiListResponse,
} from "testing/utils";

import InventoryGrid from "components/inventory/InventoryGrid/InventoryGrid";
import { get } from "api/api";
import { mockHardware } from "testing/mockData";
import { makeStore } from "slices/store";
import { getHardwareWithFilters } from "slices/hardware/hardwareSlice";

jest.mock("api/api");
const mockedGet = get as jest.MockedFunction<typeof get>;

describe("<InventoryGrid />", () => {
it("Renders all hardware from the store", async () => {
// To populate the store, dispatch a thunk while mocking the API response
const apiResponse = makeMockApiListResponse(mockHardware);

mockedGet.mockResolvedValue(apiResponse);

const store = makeStore();
store.dispatch(getHardwareWithFilters());

const { getByText } = render(<InventoryGrid />, { store });

await waitFor(() => {
mockHardware.forEach((hardware) =>
expect(getByText(hardware.name)).toBeInTheDocument()
);
});
});

it("Displays a linear progress when loading", async () => {
const apiResponse = makeMockApiListResponse(mockHardware);

mockedGet.mockReturnValue(promiseResolveWithDelay(apiResponse, 500));

const store = makeStore();
store.dispatch(getHardwareWithFilters());

const { getByText, queryByTestId } = render(<InventoryGrid />, { store });

await waitFor(() => {
expect(queryByTestId("linear-progress")).toBeInTheDocument();
});

// After results have loaded, progress bar should disappear
await waitFor(() => {
expect(queryByTestId("linear-progress")).not.toBeInTheDocument();
expect(getByText(mockHardware[0].name)).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from "react";
import Grid from "@material-ui/core/Grid";
import styles from "pages/Inventory/Inventory.module.scss";
import Item from "components/inventory/Item/Item";
import { useDispatch, useSelector } from "react-redux";
// import {
// getUpdatedHardwareDetails,
// hardwareSelectors,
// isLoadingSelector,
// } from "slices/hardware/hardwareSlice";
import {
getUpdatedHardware3dDetails,
hardware3dSelectors,
is3dLoadingSelector,
} from "slices/hardware/hardware3dSlice";
import { LinearProgress } from "@material-ui/core";
import hardwareImagePlaceholder from "assets/images/placeholders/no-hardware-image.svg";
import { openProductOverview } from "slices/ui/uiSlice";

export const InventoryGrid = () => {
const dispatch = useDispatch();
const items = useSelector(hardware3dSelectors.selectAll);
const isLoading = useSelector(is3dLoadingSelector);

const openProductOverviewPanel = (hardwareId: number) => {
dispatch(getUpdatedHardware3dDetails(hardwareId));
dispatch(openProductOverview());
};

return isLoading ? (
<LinearProgress
style={{ width: "100%", marginBottom: "10px" }}
data-testid="linear-progress"
/>
) : (
<Grid direction="row" spacing={2} container>
{items.length > 0 &&
items.map((item) => (
<Grid
xs={6}
sm={4}
md={3}
lg={2}
xl={1}
className={styles.Item}
key={item.id}
item
onClick={() => openProductOverviewPanel(item.id)}
>
<Item
image={
item.picture ??
item.image_url ??
hardwareImagePlaceholder
}
title={item.name}
total={item.quantity_available}
currentStock={item.quantity_remaining}
/>
</Grid>
))}
</Grid>
);
};

export default InventoryGrid;
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from "react";
import { DeepPartial } from "redux";

import { render, fireEvent, waitFor } from "testing/utils";

import InventorySearch from "components/inventory/InventorySearch/InventorySearch";
import { get } from "api/api";
import { HardwareFilters } from "api/types";
import {
hardwareReducerName,
initialState,
HardwareState,
} from "slices/hardware/hardwareSlice";
import { RootState } from "slices/store";

jest.mock("api/api");

const makeState = (overrides: Partial<HardwareState>): DeepPartial<RootState> => ({
[hardwareReducerName]: {
...initialState,
...overrides,
},
});

const hardwareUri = "/api/hardware/hardware/";

describe("<InventorySearch />", () => {
it("Submits search query", async () => {
const { getByLabelText } = render(<InventorySearch />);

const input = getByLabelText(/search items/i);

fireEvent.change(input, { target: { value: "foobar" } });
fireEvent.submit(input);

const expectedFilters: HardwareFilters = {
search: "foobar",
};

await waitFor(() => {
expect(get).toHaveBeenCalledWith(hardwareUri, expectedFilters);
});
});

it("Submits search query when clicking search button", async () => {
const { getByLabelText, getByTestId } = render(<InventorySearch />);

const input = getByLabelText(/search items/i);
const searchButton = getByTestId("search-button");

fireEvent.change(input, { target: { value: "foobar" } });
fireEvent.click(searchButton);

const expectedFilters: HardwareFilters = {
search: "foobar",
};

await waitFor(() => {
expect(get).toHaveBeenCalledWith(hardwareUri, expectedFilters);
});
});

it("Changes only the search filter when submitted", async () => {
const initialFilters: HardwareFilters = {
in_stock: true,
ordering: "-name",
category_ids: [1, 2],
search: "abc123",
};

const preloadedState = makeState({ filters: initialFilters });

const { getByLabelText } = render(<InventorySearch />, { preloadedState });

const input = getByLabelText(/search items/i);
fireEvent.change(input, { target: { value: "foobar" } });
fireEvent.submit(input);

const expectedFilters = {
...initialFilters,
search: "foobar",
};

await waitFor(() => {
expect(get).toHaveBeenCalledWith(hardwareUri, expectedFilters);
});
});

it("Removes the search filter and submits when cleared", async () => {
const initialFilters: HardwareFilters = {
in_stock: true,
ordering: "-name",
category_ids: [1, 2],
search: "abc123",
};

const preloadedState = makeState({ filters: initialFilters });

const { getByTestId } = render(<InventorySearch />, { preloadedState });

const clearButton = getByTestId("clear-button");
fireEvent.click(clearButton);

const { search, ...expectedFilters } = initialFilters;

await waitFor(() => {
expect(get).toHaveBeenCalledWith(hardwareUri, expectedFilters);
});
});
});
Loading
Loading