Skip to content

Commit

Permalink
Merge pull request #321 from NIAEFEUP/feature/disabled-offer-indicator
Browse files Browse the repository at this point in the history
Show indication that an offer is disabled in the "Offers management" page
  • Loading branch information
Process-ing authored Sep 11, 2023
2 parents c9be64e + 692d8a1 commit 0cab005
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 68 deletions.
7 changes: 6 additions & 1 deletion src/components/Company/Offers/Manage/CompanyOffersActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ const CompanyOffersActions = ({
<TableCell align="right">
{ !isMobile ? (
<>
<CompanyOffersVisibilityActions offer={row?.payload.offer} />
<CompanyOffersVisibilityActions
offer={row?.payload.offer}
getOfferVisibility={row?.payload.getOfferVisibility}
setOfferVisibility={row?.payload.setOfferVisibility}
offerId={row?.payload.offerId}
/>
<Tooltip title="Edit Offer">
<Link to={editOfferRoute}>
<IconButton aria-label="Edit Offer">
Expand Down
109 changes: 79 additions & 30 deletions src/components/Company/Offers/Manage/CompanyOffersManagementWidget.js
Original file line number Diff line number Diff line change
@@ -1,36 +1,46 @@
import { Divider, Grid, IconButton, makeStyles, Tooltip, Typography } from "@material-ui/core";
import { Edit as EditIcon } from "@material-ui/icons";
import { format, parseISO } from "date-fns";
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import React, { useCallback, useEffect, useState } from "react";
import { connect } from "react-redux";
import { Link } from "react-router-dom";
import { addSnackbar } from "../../../../actions/notificationActions";
import useSession from "../../../../hooks/useSession";
import { fetchCompanyOffers } from "../../../../services/companyOffersService";
import ControlledSortableSelectableTable from "../../../../utils/Table/ControlledSortableSelectableTable";
import FilterableTable from "../../../../utils/Table/FilterableTable";
import { alphabeticalSorter, GenerateTableCellFromField } from "../../../../utils/Table/utils";
import { columns } from "./CompanyOffersManagementSchema";
import PropTypes from "prop-types";
import useSession from "../../../../hooks/useSession";
import { OfferTitleFilter, PublishDateFilter, PublishEndDateFilter, LocationFilter } from "../Filters/index";
import { Edit as EditIcon } from "@material-ui/icons";
import { Link } from "react-router-dom";
import { addSnackbar } from "../../../../actions/notificationActions";
import { connect } from "react-redux";
import { RowActions } from "./CompanyOffersActions";
import Offer from "../../../HomePage/SearchResultsArea/Offer/Offer";
import { OfferConstants } from "../../../Offers/Form/OfferUtils";
import { LocationFilter, OfferTitleFilter, PublishDateFilter, PublishEndDateFilter } from "../Filters/index";
import { RowActions } from "./CompanyOffersActions";
import { columns } from "./CompanyOffersManagementSchema";
import OfferTitle from "./CompanyOffersTitle";
import CompanyOffersVisibilityActions from "./CompanyOffersVisibilityActions";

const generateRow = ({
title, location, publishDate, publishEndDate,
ownerName, _id, ...args }) => ({
title, location, publishDate, publishEndDate, isHidden, isArchived, hiddenReason,
ownerName, getOfferVisibility, setOfferVisibility, offerId, _id, ...args }) => ({
fields: {
title: { value: title, align: "left", linkDestination: `/offer/${_id}` },
title: { value: (
<OfferTitle
title={title}
getOfferVisibility={getOfferVisibility}
offerId={offerId}
/>), align: "left", linkDestination: `/offer/${_id}` },
publishStartDate: { value: format(parseISO(publishDate), "yyyy-MM-dd") },
publishEndDate: { value: format(parseISO(publishEndDate), "yyyy-MM-dd") },
location: { value: location },
},
payload: {
offer: new Offer({
title, location, publishDate, publishEndDate,
ownerName, _id, ...args,
title, location, publishDate, publishEndDate, isHidden,
isArchived, hiddenReason, ownerName, _id, ...args,
}),
getOfferVisibility: getOfferVisibility,
setOfferVisibility: setOfferVisibility,
offerId: offerId,
},
});

Expand Down Expand Up @@ -65,21 +75,29 @@ const filters = [
const CompanyOffersManagementWidget = ({ addSnackbar, isMobile }) => {
const { data, isLoggedIn } = useSession();
const [offers, setOffers] = useState({});
const [fetchedOffers, setFetchedOffers] = useState([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState(null);
const mobileCols = ["title", "publishStartDate", "actions"];
const [offerVisibilityStates, setOfferVisibilityStates] = useState([]);

const getOfferVisibilityState = useCallback(
(offerId) => offerVisibilityStates[offerId],
[offerVisibilityStates]
);

const setOfferVisibilityState = useCallback((offerId, stateFunc) => {
const newVisibilityStates = [...offerVisibilityStates];
newVisibilityStates[offerId] = stateFunc(newVisibilityStates[offerId]);
setOfferVisibilityStates(newVisibilityStates);
}, [offerVisibilityStates, setOfferVisibilityStates]);

useEffect(() => {
if (isLoggedIn) fetchCompanyOffers(data.company._id).then((offers) => {
if (Array.isArray(offers)) {
const fetchedRows = offers.reduce((rows, row) => {
rows[row._id] = generateRow(row);
return rows;
}, {});

setOffers(fetchedRows);
setFetchedOffers(offers);
} else {
setOffers({});
setFetchedOffers([]);
}
setIsLoading(false);
}).catch((err) => {
Expand All @@ -92,19 +110,44 @@ const CompanyOffersManagementWidget = ({ addSnackbar, isMobile }) => {
});
}, [addSnackbar, data.company._id, isLoggedIn]);

const RowContent = ({ rowKey, labelId }) => {
useEffect(() => {
if (Array.isArray(fetchedOffers)) {
const newVisibilityStates = fetchedOffers.map((offer) => ({
isHidden: offer.isHidden && offer.hiddenReason === OfferConstants.COMPANY_REQUEST,
isDisabled: offer.isHidden && offer.hiddenReason === OfferConstants.ADMIN_REQUEST,
isVisible: !offer.isHidden && !offer.isArchived,
isBlocked: offer.isHidden && offer.hiddenReason === OfferConstants.COMPANY_BLOCKED,
isArchived: offer.isArchived,
}));
setOfferVisibilityStates(newVisibilityStates);
}
}, [fetchedOffers]);

useEffect(() => {
if (Array.isArray(fetchedOffers)) {
const fetchedRows = fetchedOffers.reduce((rows, row, i) => {
rows[row._id] = generateRow(
{ ...row, getOfferVisibility: getOfferVisibilityState, setOfferVisibility: setOfferVisibilityState, offerId: i }
);
return rows;
}, {});
setOffers(fetchedRows);
}
}, [setOffers, setOfferVisibilityState, fetchedOffers, getOfferVisibilityState]);

const RowContent = useCallback(({ rowKey, labelId }) => {
const fields = offers[rowKey].fields;

return (
<>
{!isMobile ? Object.entries(fields).map(([fieldId, fieldOptions], i) => (
GenerateTableCellFromField(i, fieldId, fieldOptions, labelId)
GenerateTableCellFromField(i, fieldId, fieldOptions, labelId, true)
)) : Object.entries(fields).filter(([fieldId, _]) => mobileCols.includes(fieldId)).map(([fieldId, fieldOptions], i) => (
GenerateTableCellFromField(i, fieldId, fieldOptions, labelId)
GenerateTableCellFromField(i, fieldId, fieldOptions, labelId, true)
))}
</>
);
};
}, [isMobile, mobileCols, offers]);

RowContent.propTypes = {
rowKey: PropTypes.string.isRequired,
Expand All @@ -126,10 +169,11 @@ const CompanyOffersManagementWidget = ({ addSnackbar, isMobile }) => {
},
}));

const RowCollapseComponent = ({ rowKey }) => {
const classes = useRowCollapseStyles();

const RowCollapseComponent = useCallback(({ rowKey }) => {
const row = offers[rowKey];
const offerRoute = `/offer/${rowKey}`;
const classes = useRowCollapseStyles();
const mobileFieldKeys = ["location", "publishEndDate"];

return (
Expand All @@ -143,7 +187,12 @@ const CompanyOffersManagementWidget = ({ addSnackbar, isMobile }) => {
</Typography>
</Grid>
<Grid item xs={6} justifyContent="center">
<CompanyOffersVisibilityActions offer={row?.payload.offer} />
<CompanyOffersVisibilityActions
offer={row?.payload.offer}
getOfferVisibility={row?.payload.getOfferVisibility}
setOfferVisibility={row?.payload.setOfferVisibility}
offerId={row?.payload.offerId}
/>
<Tooltip title="Edit Offer">
<Link to={offerRoute}>
<IconButton aria-label="Edit Offer">
Expand All @@ -169,7 +218,7 @@ const CompanyOffersManagementWidget = ({ addSnackbar, isMobile }) => {
</>
)
);
};
}, [classes.collapsableTitles, classes.payloadSection, isMobile, offers]);

RowCollapseComponent.propTypes = {
rowKey: PropTypes.string.isRequired,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { createTheme } from "@material-ui/core";
import { SnackbarProvider } from "notistack";
import Notifier from "../../../Notifications/Notifier";
import { format, parseISO } from "date-fns";
import { OfferConstants } from "../../../Offers/Form/OfferUtils";

jest.mock("../../../../hooks/useSession");
jest.mock("../../../../services/companyOffersService");
Expand Down Expand Up @@ -55,6 +56,7 @@ describe("App", () => {
publishEndDate: "2021-09",
description: "Offer description 2",
isHidden: true,
hiddenReason: OfferConstants.COMPANY_REQUEST,
},
{
_id: "random uuid6",
Expand All @@ -67,7 +69,8 @@ describe("App", () => {
publishEndDate: "2021-09",
description: "Offer description 3",
isHidden: true,
hiddenReason: "ADMIN_REQUEST",
hiddenReason: OfferConstants.ADMIN_REQUEST,
isArchived: true,
},
];

Expand Down Expand Up @@ -213,8 +216,6 @@ describe("App", () => {
});

it("Should render mobile collapsable content on mobile device", async () => {
addSnackbar.mockImplementationOnce(() => ({ type: "" }));

const MOBILE_WIDTH_PX = 360;
window.matchMedia = createMatchMedia(MOBILE_WIDTH_PX);

Expand Down Expand Up @@ -258,6 +259,7 @@ describe("App", () => {
companyOffersService.fetchCompanyOffers.mockImplementationOnce(() => new Promise((resolve) =>
resolve([offer])
));
addSnackbar.mockImplementationOnce(() => ({ type: "" }));
hideOfferService.mockImplementation(() => new Promise((resolve) => resolve()));
enableOfferService.mockImplementation(() => new Promise((resolve) => resolve()));

Expand All @@ -282,6 +284,7 @@ describe("App", () => {

expect(queryByTestId(offerRow, "HideOffer")).not.toBeInTheDocument();
expect(getByTestId(offerRow, "EnableOffer")).toBeInTheDocument();
expect(getByTestId(offerRow, "HiddenChip")).toBeInTheDocument();

visibilityButton = getByTestId(offerRow, "EnableOffer");

Expand All @@ -291,13 +294,15 @@ describe("App", () => {

expect(getByTestId(offerRow, "HideOffer")).toBeInTheDocument();
expect(queryByTestId(offerRow, "EnableOffer")).not.toBeInTheDocument();
expect(queryByTestId(offerRow, "HiddenChip")).not.toBeInTheDocument();
});

it("Should disable hide/enable offer button when the offer is disabled by an admin", async () => {
const offer = MOCK_OFFERS[2];
companyOffersService.fetchCompanyOffers.mockImplementationOnce(() => new Promise((resolve) =>
resolve([offer])
));
addSnackbar.mockImplementationOnce(() => ({ type: "" }));
hideOfferService.mockImplementation(() => new Promise((resolve) => resolve()));
enableOfferService.mockImplementation(() => new Promise((resolve) => resolve()));

Expand Down Expand Up @@ -355,4 +360,35 @@ describe("App", () => {

expect(addSnackbar).toHaveBeenCalledTimes(1);
});

it("Should generate the right offer status chips", async () => {
companyOffersService.fetchCompanyOffers.mockImplementationOnce(() => new Promise((resolve) =>
resolve(MOCK_OFFERS)
));

await act(() =>
renderWithStoreAndTheme(
<BrowserRouter>
<MuiPickersUtilsProvider utils={DateFnsUtils}>
<CompanyOffersManagementWidget />
</MuiPickersUtilsProvider>
</BrowserRouter>, { initialState: {}, theme }
)
);

let offerRow = screen.queryByText(MOCK_OFFERS[0].title).closest("tr");
expect(queryByTestId(offerRow, "HiddenChip")).not.toBeInTheDocument();
expect(queryByTestId(offerRow, "BlockedChip")).not.toBeInTheDocument();
expect(queryByTestId(offerRow, "ArchivedChip")).not.toBeInTheDocument();

offerRow = screen.queryByText(MOCK_OFFERS[1].title).closest("tr");
expect(getByTestId(offerRow, "HiddenChip")).toBeInTheDocument();
expect(queryByTestId(offerRow, "BlockedChip")).not.toBeInTheDocument();
expect(queryByTestId(offerRow, "ArchivedChip")).not.toBeInTheDocument();

offerRow = screen.queryByText(MOCK_OFFERS[2].title).closest("tr");
expect(queryByTestId(offerRow, "HiddenChip")).not.toBeInTheDocument();
expect(getByTestId(offerRow, "BlockedChip")).toBeInTheDocument();
expect(getByTestId(offerRow, "ArchivedChip")).toBeInTheDocument();
});
});
64 changes: 64 additions & 0 deletions src/components/Company/Offers/Manage/CompanyOffersTitle.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import React, { useEffect, useState } from "react";
import PropTypes from "prop-types";
import { Chip, makeStyles } from "@material-ui/core";

const useStyles = makeStyles((theme) => ({
hiddenChip: {
backgroundColor: "#90A4AE",
marginRight: theme.spacing(.5),
},
blockedChip: {
backgroundColor: "#DC4338",
marginRight: theme.spacing(.5),
},
archivedChip: {
backgroundColor: "#56A8D6",
marginRight: theme.spacing(.5),
},
chips: {
position: "absolute",
},
}));

const OfferTitle = ({ title, getOfferVisibility, offerId }) => {
const [chips, setChips] = useState([]);
const isHidden = getOfferVisibility(offerId)?.isHidden;
const isBlocked = getOfferVisibility(offerId)?.isDisabled;
const isArchived = getOfferVisibility(offerId)?.isArchived;

const classes = useStyles();

useEffect(() => {
const statusChips = {
hidden: <Chip size="small" label="Hidden" data-testid="HiddenChip" className={classes.hiddenChip} />,
blocked: <Chip size="small" label="Blocked" data-testid="BlockedChip" className={classes.blockedChip} />,
archived: <Chip size="small" label="Archived" data-testid="ArchivedChip" className={classes.archivedChip} />,
};

const tempChips = [];
if (isHidden)
tempChips.push(statusChips.hidden);
if (isBlocked)
tempChips.push(statusChips.blocked);
if (isArchived)
tempChips.push(statusChips.archived);
setChips(tempChips);
}, [classes, isArchived, isBlocked, isHidden]);

return (
<>
{title}
<div className={classes.chips}>
{chips}
</div>
</>
);
};

OfferTitle.propTypes = {
title: PropTypes.string.isRequired,
getOfferVisibility: PropTypes.func.isRequired,
offerId: PropTypes.number.isRequired,
};

export default OfferTitle;
Loading

0 comments on commit 0cab005

Please sign in to comment.