diff --git a/src/components/Company/Offers/Manage/CompanyOffersActions.js b/src/components/Company/Offers/Manage/CompanyOffersActions.js index b5ff2131..45b076a2 100644 --- a/src/components/Company/Offers/Manage/CompanyOffersActions.js +++ b/src/components/Company/Offers/Manage/CompanyOffersActions.js @@ -26,7 +26,12 @@ const CompanyOffersActions = ({ { !isMobile ? ( <> - + diff --git a/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.js b/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.js index 2a150eba..530e9725 100644 --- a/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.js +++ b/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.js @@ -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: ( + ), 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, }, }); @@ -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) => { @@ -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, @@ -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 ( @@ -143,7 +187,12 @@ const CompanyOffersManagementWidget = ({ addSnackbar, isMobile }) => { - + @@ -169,7 +218,7 @@ const CompanyOffersManagementWidget = ({ addSnackbar, isMobile }) => { ) ); - }; + }, [classes.collapsableTitles, classes.payloadSection, isMobile, offers]); RowCollapseComponent.propTypes = { rowKey: PropTypes.string.isRequired, diff --git a/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.spec.js b/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.spec.js index 57ea6e66..405f2a90 100644 --- a/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.spec.js +++ b/src/components/Company/Offers/Manage/CompanyOffersManagementWidget.spec.js @@ -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"); @@ -55,6 +56,7 @@ describe("App", () => { publishEndDate: "2021-09", description: "Offer description 2", isHidden: true, + hiddenReason: OfferConstants.COMPANY_REQUEST, }, { _id: "random uuid6", @@ -67,7 +69,8 @@ describe("App", () => { publishEndDate: "2021-09", description: "Offer description 3", isHidden: true, - hiddenReason: "ADMIN_REQUEST", + hiddenReason: OfferConstants.ADMIN_REQUEST, + isArchived: true, }, ]; @@ -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); @@ -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())); @@ -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"); @@ -291,6 +294,7 @@ 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 () => { @@ -298,6 +302,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())); @@ -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( + + + + + , { 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(); + }); }); diff --git a/src/components/Company/Offers/Manage/CompanyOffersTitle.js b/src/components/Company/Offers/Manage/CompanyOffersTitle.js new file mode 100644 index 00000000..ff5c7c57 --- /dev/null +++ b/src/components/Company/Offers/Manage/CompanyOffersTitle.js @@ -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: , + blocked: , + archived: , + }; + + 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} +
+ {chips} +
+ + ); +}; + +OfferTitle.propTypes = { + title: PropTypes.string.isRequired, + getOfferVisibility: PropTypes.func.isRequired, + offerId: PropTypes.number.isRequired, +}; + +export default OfferTitle; diff --git a/src/components/Company/Offers/Manage/CompanyOffersVisibilityActions.js b/src/components/Company/Offers/Manage/CompanyOffersVisibilityActions.js index ae1fa580..2249fd32 100644 --- a/src/components/Company/Offers/Manage/CompanyOffersVisibilityActions.js +++ b/src/components/Company/Offers/Manage/CompanyOffersVisibilityActions.js @@ -16,38 +16,30 @@ import { getHumanError } from "../../../../utils/offer/OfferUtils"; import { useDispatch } from "react-redux"; import { addSnackbar as addSnackbarAction } from "../../../../actions/notificationActions"; import Offer from "../../../HomePage/SearchResultsArea/Offer/Offer"; -import { OfferConstants } from "../../../Offers/Form/OfferUtils"; -const CompanyOffersVisibilityActions = ({ offer }) => { +const CompanyOffersVisibilityActions = ({ offer, getOfferVisibility, setOfferVisibility, offerId }) => { const dispatch = useDispatch(); const addSnackbar = useCallback((notification) => dispatch(addSnackbarAction(notification)), [dispatch]); - const [offerVisibilityState, setOfferVisibilityState] = useState({ - isVisible: undefined, - isDisabled: undefined, - isBlocked: undefined, - }); + const offerVisible = getOfferVisibility(offerId)?.isVisible; + const offerDisabled = getOfferVisibility(offerId)?.isDisabled; + const offerBlocked = getOfferVisibility(offerId)?.isBlocked; const [loadingOfferVisibility, setLoadingOfferVisibility] = useState(false); - - const isHiddenOffer = offer?.isHidden; - const offerHiddenReason = offer?.hiddenReason; + const [offerVisibilityButtonDisabled, setOfferVisibilityButtonDisabled] = useState(false); useEffect(() => { - setOfferVisibilityState({ - isDisabled: isHiddenOffer && offerHiddenReason === OfferConstants.ADMIN_REQUEST, - isVisible: !isHiddenOffer, - isBlocked: isHiddenOffer && offerHiddenReason === OfferConstants.COMPANY_BLOCKED, - }); - }, [isHiddenOffer, offerHiddenReason]); - + setOfferVisibilityButtonDisabled(loadingOfferVisibility + || offerDisabled + || offerBlocked); + }, [loadingOfferVisibility, offerBlocked, offerDisabled, offerId]); const handleHideOffer = useCallback(({ offer, addSnackbar, onError }) => { setLoadingOfferVisibility(true); hideOfferService(offer._id) .then(() => { - setOfferVisibilityState((offerVisibilityState) => ({ ...offerVisibilityState, isVisible: false })); + setOfferVisibility(offerId, (state) => ({ ...state, isVisible: false, isHidden: true })); addSnackbar({ message: "The offer was hidden", key: `${Date.now()}-${offer._id}-hidden`, @@ -56,13 +48,13 @@ const CompanyOffersVisibilityActions = ({ offer }) => { .catch((err) => { if (onError) onError(err); }).finally(() => setLoadingOfferVisibility(false)); - }, []); + }, [offerId, setOfferVisibility]); const handleEnableOffer = useCallback(({ offer, addSnackbar, onError }) => { setLoadingOfferVisibility(true); enableOfferService(offer._id) .then(() => { - setOfferVisibilityState((offerVisibilityState) => ({ ...offerVisibilityState, isVisible: true })); + setOfferVisibility(offerId, (state) => ({ ...state, isVisible: true, isHidden: false })); addSnackbar({ message: "The offer was enabled", key: `${Date.now()}-${offer._id}-enabled`, @@ -71,7 +63,7 @@ const CompanyOffersVisibilityActions = ({ offer }) => { .catch((err) => { if (onError) onError(err); }).finally(() => setLoadingOfferVisibility(false)); - }, []); + }, [offerId, setOfferVisibility]); const handleOfferVisibilityError = useCallback((err) => { if (Array.isArray(err) && err.length > 0) { @@ -88,8 +80,8 @@ const CompanyOffersVisibilityActions = ({ offer }) => { } }, [addSnackbar, offer._id]); - const handleOfferVisibility = () => { - if (offerVisibilityState.isVisible) { + const handleOfferVisibility = useCallback(() => { + if (offerVisible) { handleHideOffer({ offer: offer, addSnackbar: addSnackbar, @@ -102,18 +94,16 @@ const CompanyOffersVisibilityActions = ({ offer }) => { onError: handleOfferVisibilityError, }); } - }; - - const offerVisibilityButtonDisabled = loadingOfferVisibility || offerVisibilityState.isDisabled || offerVisibilityState.isBlocked; + }, [offerVisible, handleHideOffer, offer, addSnackbar, handleOfferVisibilityError, handleEnableOffer]); return ( - + - {offerVisibilityState.isVisible ? + {offerVisible ? { }; CompanyOffersVisibilityActions.propTypes = { - offer: PropTypes.instanceOf(Offer), + offer: PropTypes.instanceOf(Offer).isRequired, + getOfferVisibility: PropTypes.func.isRequired, + setOfferVisibility: PropTypes.func.isRequired, + offerId: PropTypes.number.isRequired, }; export default CompanyOffersVisibilityActions; diff --git a/src/utils/Table/utils.js b/src/utils/Table/utils.js index 54ec820b..95e39bf3 100644 --- a/src/utils/Table/utils.js +++ b/src/utils/Table/utils.js @@ -21,7 +21,7 @@ const useStyles = makeStyles({ }, }); -export const GenerateTableCellFromField = (id, fieldId, fieldOptions, labelId) => { +export const GenerateTableCellFromField = (id, fieldId, fieldOptions, labelId, extended = false) => { const classes = useStyles(); const linkDestination = fieldOptions?.linkDestination; @@ -34,10 +34,12 @@ export const GenerateTableCellFromField = (id, fieldId, fieldOptions, labelId) = id={id === 0 ? `${labelId}-label` : undefined} align={fieldOptions.align || "right"} > - {fieldOptions?.linkDestination ? - - {fieldOptions?.value} - : fieldOptions.value} +
+ {fieldOptions?.linkDestination ? + + {fieldOptions?.value} + : fieldOptions.value} +
); }