diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json
index 0c996a914..3c43c4081 100644
--- a/app/i18n/messages/en.json
+++ b/app/i18n/messages/en.json
@@ -52,6 +52,7 @@
"common.cancel": "Cancel",
"common.cancelled": "Cancelled",
"common.cancelling": "Cancelling...",
+ "common.cancelReservations": "Cancel reservations",
"common.checkError": "Error: ",
"common.customerGroup": "Customer Group",
"common.comments": "Comments",
@@ -176,6 +177,18 @@
"MapToggle.resultsText": "{count} {count, plural, one {premise} other {premises}} found",
"MapToggle.showList": "List",
"MapToggle.showMap": "Map",
+ "MassCancel.cancelReservations": "Cancel reservations",
+ "MassCancel.confirmCancel": "I am sure that I want to cancel all reservations for the period",
+ "MassCancel.datetime.endLabel": "Cancel reservations to",
+ "MassCancel.datetime.startLabel": "Cancel reservations from",
+ "MassCancel.error.confirmCancel": "Accept cancellations",
+ "MassCancel.error.endBeforeStart": "End time is before the start time",
+ "MassCancel.error.required.end": "End time is required",
+ "MassCancel.error.required.resource": "Resource is required",
+ "MassCancel.error.required.start": "Start time is required",
+ "MassCancel.error.startAfterEnd": "Start time is after the end time",
+ "MassCancel.instructions": "Cancel all reservations for the selected resource within the given time range. Customers will be notified of the cancellations as usual. Only unit admins can make cancellations through this method. Please note that once cancellations are made, they cannot be undone or reverted to the previous state.",
+ "MassCancel.resourceLabel": "Resource",
"MiniModal.buttonText": "Done",
"ModalHeader.closeButtonText": "Close",
"Nav.FontSize.title": "Fontsize",
@@ -222,6 +235,8 @@
"Notifications.loginMessage": "Sign in to continue.",
"Notifications.loginToReserve": "Log in to reserve these premises.",
"Notifications.loginToReserveStrongAuth": "Log in with Suomi.fi to reserve these premises.",
+ "Notifications.massCancelSuccessful": "Cancellations of the reservations were successful.",
+ "Notifications.noPermission": "You do not have permission to perform this action.",
"Notifications.noRightToReserve": "You do not have the right to make this reservation.",
"Notifications.reservationDeleteSuccessMessage": "Your reservation was successfully cancelled.",
"Notifications.reservationUpdateSuccessMessage": "Your reservation has been updated.",
diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json
index 0081c5347..62492131a 100644
--- a/app/i18n/messages/fi.json
+++ b/app/i18n/messages/fi.json
@@ -52,6 +52,7 @@
"common.cancel": "Peru",
"common.cancelled": "Peruttu",
"common.cancelling": "Perutaan...",
+ "common.cancelReservations": "Peruuta varauksia",
"common.checkError": "Tarkista: ",
"common.customerGroup": "Asiakasryhmä",
"common.comments": "Kommentit",
@@ -176,6 +177,18 @@
"MapToggle.resultsText": "Tiloja löytyi {count} {count, plural, one {kpl} other {kpl}}",
"MapToggle.showList": "Lista",
"MapToggle.showMap": "Kartta",
+ "MassCancel.cancelReservations": "Peruuta varaukset",
+ "MassCancel.confirmCancel": "Olen varma, että haluan peruuttaa kaikki varaukset aikaväliltä",
+ "MassCancel.datetime.endLabel": "Peruuta varaukset päättyen",
+ "MassCancel.datetime.startLabel": "Peruuta varaukset alkaen",
+ "MassCancel.error.confirmCancel": "Hyväksy peruutukset",
+ "MassCancel.error.endBeforeStart": "Loppuaika on ennen alkuaikaa",
+ "MassCancel.error.required.end": "Loppuaika on pakollinen tieto",
+ "MassCancel.error.required.resource": "Resurssi on pakollinen tieto",
+ "MassCancel.error.required.start": "Alkuaika on pakollinen tieto",
+ "MassCancel.error.startAfterEnd": "Alkuaika on loppuaikaa myöhemmin",
+ "MassCancel.instructions": "Peruuta kaikki valitun resurssin varaukset annetulta aikaväliltä. Tieto peruutuksista lähetetään asiakkaille normaaliin tapaan. Peruutuksia voi tehdä tätä kautta vain toimipisteen pääkäyttäjät. Tehtyjä peruutuksia ei voi jälkikäteen enää kumota eli palauttaa aikaisempaan tilaan.",
+ "MassCancel.resourceLabel": "Resurssi",
"MiniModal.buttonText": "Valmis",
"ModalHeader.closeButtonText": "Sulje",
"Nav.FontSize.title": "Tekstikoko",
@@ -222,6 +235,8 @@
"Notifications.loginMessage": "Kirjaudu sisään jatkaaksesi.",
"Notifications.loginToReserve": "Kirjaudu sisään tehdäksesi varauksen tähän tilaan.",
"Notifications.loginToReserveStrongAuth": "Kirjaudu sisään Suomi.fi:llä tehdäksesi varauksen tähän tilaan.",
+ "Notifications.massCancelSuccessful": "Varausten peruminen onnistui.",
+ "Notifications.noPermission": "Sinulla ei ole oikeutta suorittaa tätä toimintoa.",
"Notifications.noRightToReserve": "Sinulla ei ole oikeutta tehdä tätä varausta.",
"Notifications.reservationDeleteSuccessMessage": "Varauksen peruminen onnistui.",
"Notifications.reservationUpdateSuccessMessage": "Varaus päivitetty.",
diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json
index aa1d56269..257cde4be 100644
--- a/app/i18n/messages/sv.json
+++ b/app/i18n/messages/sv.json
@@ -52,6 +52,7 @@
"common.cancel": "Avbryt",
"common.cancelled": "Avbokad",
"common.cancelling": "Avbokas...",
+ "common.cancelReservations": "Avboka bokningar",
"common.checkError": "Kontrollera: ",
"common.customerGroup": "Kundgrupp",
"common.comments": "Kommentarer",
@@ -178,6 +179,18 @@
"MapToggle.resultsText": "{count} {count, plural, one {rum} other {rum}} hittades",
"MapToggle.showList": "Resultat",
"MapToggle.showMap": "Karta",
+ "MassCancel.cancelReservations": "Avbryt Reservationer",
+ "MassCancel.confirmCancel": "Jag är säker på att jag vill avboka alla reservationer för perioden",
+ "MassCancel.datetime.endLabel": "Avboka reservationer till",
+ "MassCancel.datetime.startLabel": "Avboka reservationer från",
+ "MassCancel.error.confirmCancel": "Godkänn avbokningar",
+ "MassCancel.error.endBeforeStart": "Sluttiden är före starttiden",
+ "MassCancel.error.required.end": "Sluttid krävs",
+ "MassCancel.error.required.resource": "Resurs krävs",
+ "MassCancel.error.required.start": "Starttid krävs",
+ "MassCancel.error.startAfterEnd": "Starttiden är senare än sluttiden",
+ "MassCancel.instructions": "Avboka alla reservationer för den valda resursen inom den angivna tidsperioden. Kunderna kommer att informeras om avbokningarna som vanligt. Endast enhetsadministratörer kan göra avbokningar via denna metod. Observera att när avbokningar har gjorts kan de inte ångras eller återställas till det tidigare tillståndet.",
+ "MassCancel.resourceLabel": "Resurs",
"MiniModal.buttonText": "Klart",
"ModalHeader.closeButtonText": "Stäng",
"Nav.FontSize.title": "Textstorlek",
@@ -224,6 +237,8 @@
"Notifications.loginMessage": "Logga in för att fortsätta.",
"Notifications.loginToReserve": "Logga in för att boka detta utrymme.",
"Notifications.loginToReserveStrongAuth": "Logga in via Suomi.fi för att boka detta utrymme.",
+ "Notifications.massCancelSuccessful": "Avbokningarna lyckades.",
+ "Notifications.noPermission": "Du har inte behörighet att utföra denna åtgärd.",
"Notifications.noRightToReserve": "Du har inte rätt att göra denna bokning.",
"Notifications.reservationDeleteSuccessMessage": "Bokningen avbokades.",
"Notifications.reservationUpdateSuccessMessage": "Bokningen uppdaterades.",
diff --git a/app/pages/manage-reservations/ManageReservationsPage.js b/app/pages/manage-reservations/ManageReservationsPage.js
index d1f7b57f3..b6d88d6a1 100644
--- a/app/pages/manage-reservations/ManageReservationsPage.js
+++ b/app/pages/manage-reservations/ManageReservationsPage.js
@@ -5,6 +5,7 @@ import Loader from 'react-loader';
import Col from 'react-bootstrap/lib/Col';
import Grid from 'react-bootstrap/lib/Grid';
import Row from 'react-bootstrap/lib/Row';
+import Button from 'react-bootstrap/lib/Button';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { withRouter } from 'react-router';
@@ -37,6 +38,7 @@ import { getFilteredReservations } from './manageReservationsPageUtils';
import ReservationInfoModal from 'shared/modals/reservation-info';
import ReservationCancelModal from 'shared/modals/reservation-cancel';
import PageResultsText from './PageResultsText';
+import MassCancelModal from '../../shared/modals/reservation-mass-cancel/MassCancelModal';
class ManageReservationsPage extends React.Component {
@@ -45,6 +47,7 @@ class ManageReservationsPage extends React.Component {
this.state = {
showOnlyFilters: [constants.RESERVATION_SHOWONLY_FILTERS.CAN_MODIFY],
+ showMassCancel: false,
};
this.handleFetchReservations = this.handleFetchReservations.bind(this);
@@ -54,6 +57,16 @@ class ManageReservationsPage extends React.Component {
this.handleOpenInfoModal = this.handleOpenInfoModal.bind(this);
this.handleEditClick = this.handleEditClick.bind(this);
this.handleEditReservation = this.handleEditReservation.bind(this);
+ this.handleShowMassCancel = this.handleShowMassCancel.bind(this);
+ this.handleHideMassCancel = this.handleHideMassCancel.bind(this);
+ }
+
+ handleShowMassCancel() {
+ this.setState({ showMassCancel: true });
+ }
+
+ handleHideMassCancel() {
+ this.setState({ showMassCancel: false });
}
componentDidMount() {
@@ -171,10 +184,12 @@ class ManageReservationsPage extends React.Component {
locale,
isFetchingReservations,
isFetchingUnits,
+ fontSize,
} = this.props;
const {
showOnlyFilters,
+ showMassCancel,
} = this.state;
const filters = getFiltersFromUrl(location, false);
@@ -187,9 +202,18 @@ class ManageReservationsPage extends React.Component {
-
+
{title}
+
+
+ {t('common.cancelReservations')}
+
+
+
);
}
@@ -257,6 +286,7 @@ ManageReservationsPage.propTypes = {
reservationsTotalCount: PropTypes.number.isRequired,
isFetchingReservations: PropTypes.bool,
isFetchingUnits: PropTypes.bool,
+ fontSize: PropTypes.string,
};
export const UnwrappedManageReservationsPage = injectT(ManageReservationsPage);
diff --git a/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js b/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js
index 63171ecba..585141e1d 100644
--- a/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js
+++ b/app/pages/manage-reservations/__tests__/ManageReservationsPage.spec.js
@@ -17,6 +17,7 @@ import PageWrapper from '../../PageWrapper';
import Reservation from 'utils/fixtures/Reservation';
import { getEditReservationUrl } from 'utils/reservationUtils';
import PageResultsText from '../PageResultsText';
+import MassCancelModal from '../../../shared/modals/reservation-mass-cancel/MassCancelModal';
describe('ManageReservationsFilters', () => {
@@ -41,6 +42,7 @@ describe('ManageReservationsFilters', () => {
reservationsTotalCount: 0,
isFetchingReservations: false,
isFetchingUnits: false,
+ fontSize: '__font-size-small',
};
function getWrapper(props) {
@@ -63,6 +65,16 @@ describe('ManageReservationsFilters', () => {
expect(heading.text()).toBe('ManageReservationsPage.title');
});
+ test('mass cancel button', () => {
+ const wrapper = getWrapper();
+ const massCancelButton = wrapper.find('.app-ManageReservationsPage__filters').find('Button');
+ expect(massCancelButton).toHaveLength(1);
+ expect(massCancelButton.prop('id')).toBe('cancel-reservations-btn');
+ expect(massCancelButton.prop('onClick')).toBe(wrapper.instance().handleShowMassCancel);
+ expect(massCancelButton.prop('className')).toBe(defaultProps.fontSize);
+ expect(massCancelButton.children().at(0).text()).toBe('common.cancelReservations');
+ });
+
test('ManageReservationsFilters', () => {
const wrapper = getWrapper();
const instance = wrapper.instance();
@@ -142,6 +154,16 @@ describe('ManageReservationsFilters', () => {
const cancelModal = getWrapper().find(ReservationCancelModal);
expect(cancelModal).toHaveLength(1);
});
+
+ test('MassCancelModal', () => {
+ const wrapper = getWrapper();
+ const instance = wrapper.instance();
+ const massCancelModal = wrapper.find(MassCancelModal);
+ expect(massCancelModal).toHaveLength(1);
+ expect(massCancelModal.prop('onCancelSuccess')).toBe(instance.handleFetchReservations);
+ expect(massCancelModal.prop('onClose')).toBe(instance.handleHideMassCancel);
+ expect(massCancelModal.prop('show')).toBe(instance.state.showMassCancel);
+ });
});
describe('functions', () => {
@@ -152,6 +174,22 @@ describe('ManageReservationsFilters', () => {
});
});
+ describe('handleShowMassCancel', () => {
+ const instance = getWrapper().instance();
+ const spy = jest.spyOn(instance, 'setState');
+ instance.handleShowMassCancel();
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toStrictEqual({ showMassCancel: true });
+ });
+
+ describe('handleHideMassCancel', () => {
+ const instance = getWrapper().instance();
+ const spy = jest.spyOn(instance, 'setState');
+ instance.handleHideMassCancel();
+ expect(spy).toHaveBeenCalledTimes(1);
+ expect(spy.mock.calls[0][0]).toStrictEqual({ showMassCancel: false });
+ });
+
describe('componentDidMount', () => {
test('calls fetchUnits', () => {
const instance = getWrapper().instance();
diff --git a/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js b/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js
index 742e1ca1e..40ac45036 100644
--- a/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js
+++ b/app/pages/manage-reservations/__tests__/manageReservationsPageSelector.spec.js
@@ -47,4 +47,8 @@ describe('/pages/favorites/favoritesPageSelector', () => {
test('returns reservationsTotalCount', () => {
expect(getSelector().reservationsTotalCount).toBeDefined();
});
+
+ test('returns fontSize', () => {
+ expect(getSelector().fontSize).toBeDefined();
+ });
});
diff --git a/app/pages/manage-reservations/_manage-reservations-page.scss b/app/pages/manage-reservations/_manage-reservations-page.scss
index 8052b46c8..b2bd74188 100644
--- a/app/pages/manage-reservations/_manage-reservations-page.scss
+++ b/app/pages/manage-reservations/_manage-reservations-page.scss
@@ -12,6 +12,24 @@
margin: 32px 0 16px;
}
+ #cancel-btn-container {
+ text-align: right;
+
+ @media(max-width:$screen-sm-min) {
+ text-align: initial;
+ }
+
+ #cancel-reservations-btn {
+ margin-top: 32px;
+
+ @media(max-width:$screen-sm-min) {
+ margin-top: 0px;
+ margin-bottom: 8px;
+ }
+ }
+ }
+
+
&__filters {
background-color: #dedfe1;
padding: 0 0 40px;
@@ -20,4 +38,4 @@
&__list {
margin-top: 20px;
}
-}
+}
\ No newline at end of file
diff --git a/app/pages/manage-reservations/inputs/SelectField.js b/app/pages/manage-reservations/inputs/SelectField.js
index 4d12ecbab..dba5f205e 100644
--- a/app/pages/manage-reservations/inputs/SelectField.js
+++ b/app/pages/manage-reservations/inputs/SelectField.js
@@ -2,6 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import ControlLabel from 'react-bootstrap/lib/ControlLabel';
import FormGroup from 'react-bootstrap/lib/FormGroup';
+import HelpBlock from 'react-bootstrap/lib/HelpBlock';
import Select from 'react-select';
import injectT from '../../../i18n/injectT';
@@ -29,11 +30,18 @@ function SelectField({
placeholder = t('common.select'),
options,
value,
+ isRequired,
+ error,
}) {
return (
- {label && {label} }
+ {label && (
+
+ {label}
+ {isRequired && * }
+
+ )}
+ {error && (
+
+ {t(error)}
+
+ )}
);
@@ -74,6 +91,8 @@ SelectField.propTypes = {
PropTypes.array,
PropTypes.number,
]),
+ isRequired: PropTypes.bool,
+ error: PropTypes.string,
};
export default injectT(SelectField);
diff --git a/app/pages/manage-reservations/inputs/__tests__/SelectField.spec.js b/app/pages/manage-reservations/inputs/__tests__/SelectField.spec.js
index cf5b9a4c8..e4c97a51b 100644
--- a/app/pages/manage-reservations/inputs/__tests__/SelectField.spec.js
+++ b/app/pages/manage-reservations/inputs/__tests__/SelectField.spec.js
@@ -42,7 +42,17 @@ describe('SelectField', () => {
const label = 'test-label';
const controlLabel = getWrapper({ label }).find(ControlLabel);
expect(controlLabel).toHaveLength(1);
- expect(controlLabel.props().children).toBe(label);
+ expect(controlLabel.props().children).toStrictEqual(['test-label', undefined]);
+ });
+
+ test('when prop label is given and the field is required', () => {
+ const label = 'test-label';
+ const controlLabel = getWrapper({ label, isRequired: true }).find(ControlLabel);
+ expect(controlLabel).toHaveLength(1);
+ expect(controlLabel.props().children).toStrictEqual([
+ 'test-label',
+ * ,
+ ]);
});
test('when prop label is not given', () => {
@@ -67,5 +77,20 @@ describe('SelectField', () => {
expect(select.prop('placeholder')).toBe(defaultProps.placeholder);
expect(select.prop('value')).toBe(getOption(defaultProps.value, defaultProps.options));
});
+
+ test('HelpBlock is not rendered when error is not given', () => {
+ const helpBlock = getWrapper().find('.has-error');
+ expect(helpBlock).toHaveLength(0);
+ });
+
+ test('HelpBlock is rendered when error is given', () => {
+ const error = 'MassCancel.error.required.resource';
+ const wrapper = getWrapper({ error });
+ const helpBlock = wrapper.find('.has-error');
+ expect(helpBlock).toHaveLength(1);
+ expect(helpBlock.children().at(0).text()).toBe(error);
+ expect(helpBlock.prop('id')).toBe(`${defaultProps.id}-error`);
+ expect(helpBlock.prop('role')).toBe('alert');
+ });
});
});
diff --git a/app/pages/manage-reservations/manageReservationsPageSelector.js b/app/pages/manage-reservations/manageReservationsPageSelector.js
index 916e9f682..a118b2512 100644
--- a/app/pages/manage-reservations/manageReservationsPageSelector.js
+++ b/app/pages/manage-reservations/manageReservationsPageSelector.js
@@ -5,6 +5,7 @@ import requestIsActiveSelectorFactory from 'state/selectors/factories/requestIsA
import { isAdminSelector } from 'state/selectors/authSelectors';
import { userFavouriteResourcesSelector, unitsSelector } from 'state/selectors/dataSelectors';
import { currentLanguageSelector } from 'state/selectors/translationSelectors';
+import { fontSizeSelector } from '../../state/selectors/accessibilitySelectors';
const reservationsSelector = state => state.ui.pages.manageReservations.results;
@@ -30,6 +31,7 @@ const manageReservationsPageSelector = createStructuredSelector({
units: unitsArraySelector,
reservations: reservationsSelector,
reservationsTotalCount: reservationsCountSelector,
+ fontSize: fontSizeSelector,
});
export default manageReservationsPageSelector;
diff --git a/app/shared/modals/_modals.scss b/app/shared/modals/_modals.scss
index 60fb3de0a..60d01669e 100644
--- a/app/shared/modals/_modals.scss
+++ b/app/shared/modals/_modals.scss
@@ -3,14 +3,16 @@
@import './reservation-info/reservation-info-modal';
@import './reservation-success/reservation-success-modal';
@import './reservation-payment/reservation-payment-modal';
+@import './reservation-mass-cancel/reservation-mass-cancel-modal';
.modal-body {
overflow-y: scroll;
max-height: 70vh;
+
.modal-controls {
text-align: right;
margin: $modal-inner-padding -$modal-inner-padding 0;
padding: $modal-inner-padding $modal-inner-padding 0;
border-top: 1px solid $modal-footer-border-color;
}
-}
+}
\ No newline at end of file
diff --git a/app/shared/modals/reservation-mass-cancel/MassCancelForm.js b/app/shared/modals/reservation-mass-cancel/MassCancelForm.js
new file mode 100644
index 000000000..b815c54a2
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/MassCancelForm.js
@@ -0,0 +1,106 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import Checkbox from 'react-bootstrap/lib/Checkbox';
+import ControlLabel from 'react-bootstrap/lib/ControlLabel';
+import FormControl from 'react-bootstrap/lib/FormControl';
+import FormGroup from 'react-bootstrap/lib/FormGroup';
+import HelpBlock from 'react-bootstrap/lib/HelpBlock';
+
+import SelectField from '../../../pages/manage-reservations/inputs/SelectField';
+import injectT from '../../../i18n/injectT';
+import { mapResourceOptions } from './massCancelUtils';
+import RequiredSpan from './RequiredSpan';
+
+
+function MassCancelForm({
+ t, resources, selectedResource, setSelectedResource, errors, handleOnBlur,
+ startDate, setStartDate, endDate, setEndDate, confirmCancel, setConfirmCancel
+}) {
+ return (
+
+ {t('MassCancel.instructions')}
+ { setSelectedResource(event.value); handleOnBlur('selectedResource'); }}
+ options={mapResourceOptions(resources)}
+ value={selectedResource}
+ />
+
+
+ {t('MassCancel.datetime.startLabel')}
+
+
+ handleOnBlur('startDate')}
+ onChange={(event) => setStartDate(event.target.value)}
+ type="datetime-local"
+ value={startDate}
+ />
+ {errors.startDate && (
+
+ {t(errors.startDate)}
+
+ )}
+
+
+
+ {t('MassCancel.datetime.endLabel')}
+
+
+ handleOnBlur('endDate')}
+ onChange={(event) => setEndDate(event.target.value)}
+ type="datetime-local"
+ value={endDate}
+ />
+ {errors.endDate && (
+
+ {t(errors.endDate)}
+
+ )}
+
+ handleOnBlur('confirmCancel')}
+ onChange={(event) => setConfirmCancel(event.target.checked)}
+ >
+ {t('MassCancel.confirmCancel')}
+
+
+ );
+}
+
+MassCancelForm.propTypes = {
+ t: PropTypes.func.isRequired,
+ resources: PropTypes.arrayOf(PropTypes.object),
+ selectedResource: PropTypes.string.isRequired,
+ setSelectedResource: PropTypes.func.isRequired,
+ errors: PropTypes.object.isRequired,
+ startDate: PropTypes.string.isRequired,
+ setStartDate: PropTypes.func.isRequired,
+ endDate: PropTypes.string.isRequired,
+ setEndDate: PropTypes.func.isRequired,
+ confirmCancel: PropTypes.bool.isRequired,
+ setConfirmCancel: PropTypes.func.isRequired,
+ handleOnBlur: PropTypes.func.isRequired,
+};
+
+export default injectT(MassCancelForm);
diff --git a/app/shared/modals/reservation-mass-cancel/MassCancelModal.js b/app/shared/modals/reservation-mass-cancel/MassCancelModal.js
new file mode 100644
index 000000000..3a9433916
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/MassCancelModal.js
@@ -0,0 +1,166 @@
+import React, { useEffect, useState } from 'react';
+import PropTypes from 'prop-types';
+import Modal from 'react-bootstrap/lib/Modal';
+import Button from 'react-bootstrap/lib/Button';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import { addNotification } from 'actions/notificationsActions';
+import { fetchResources } from 'actions/resourceActions';
+import MassCancelForm from './MassCancelForm';
+import injectT from '../../../i18n/injectT';
+import { currentLanguageSelector } from '../../../state/selectors/translationSelectors';
+import { resourcesSelector } from '../../../state/selectors/dataSelectors';
+import { fontSizeSelector } from '../../../state/selectors/accessibilitySelectors';
+import { FORM_INITIAL_VALUES, sendMassCancel, validateForm } from './massCancelUtils';
+
+function MassCancelModal({
+ show, onClose, t, actions, resources, state, onCancelSuccess, fontSize
+}) {
+ const [selectedResource, setSelectedResource] = useState(FORM_INITIAL_VALUES.selectedResource);
+ const [startDate, setStartDate] = useState(FORM_INITIAL_VALUES.startDate);
+ const [endDate, setEndDate] = useState(FORM_INITIAL_VALUES.endDate);
+ const [confirmCancel, setConfirmCancel] = useState(FORM_INITIAL_VALUES.confirmCancel);
+ const [cancelling, setCancelling] = useState(FORM_INITIAL_VALUES.cancelling);
+ const [errors, setErrors] = useState(FORM_INITIAL_VALUES.errors);
+
+ useEffect(() => {
+ actions.fetchResources({ is_favorite: true });
+ }, []);
+
+ const resourceList = Object.values(resources);
+
+ const resetForm = () => {
+ setSelectedResource(FORM_INITIAL_VALUES.selectedResource);
+ setStartDate(FORM_INITIAL_VALUES.startDate);
+ setEndDate(FORM_INITIAL_VALUES.endDate);
+ setConfirmCancel(FORM_INITIAL_VALUES.confirmCancel);
+ setErrors(FORM_INITIAL_VALUES.errors);
+ };
+
+ const handleOnBlur = (target) => {
+ const newErrors = { ...errors };
+ delete newErrors[target];
+ setErrors(newErrors);
+ };
+
+ const handleSubmit = async () => {
+ const validationErrors = validateForm({
+ selectedResource, startDate, endDate, confirmCancel
+ });
+ if (Object.keys(validationErrors).length > 0) {
+ setErrors(validationErrors);
+ return;
+ }
+ setCancelling(true);
+ const result = await sendMassCancel(selectedResource, startDate, endDate, state);
+ if (result.errorOccurred) {
+ if (result.code === 403) {
+ actions.addNotification({
+ message: t('Notifications.noPermission'),
+ type: 'error',
+ timeOut: 10000,
+ });
+ } else {
+ actions.addNotification({
+ message: t('Notifications.errorMessage'),
+ type: 'error',
+ timeOut: 10000,
+ });
+ }
+ } else {
+ actions.addNotification({
+ message: t('Notifications.massCancelSuccessful'),
+ type: 'success',
+ timeOut: 10000,
+ });
+ onClose();
+ if (onCancelSuccess) {
+ resetForm();
+ onCancelSuccess();
+ }
+ }
+ setCancelling(false);
+ };
+
+ return (
+ { resetForm(); onClose(); }}
+ show={show}
+ >
+
+ {t('common.cancelReservations')}
+
+
+
+
+
+ { resetForm(); onClose(); }}
+ >
+ {t('common.back')}
+
+
+ {t('MassCancel.cancelReservations')}
+
+
+
+ );
+}
+
+MassCancelModal.propTypes = {
+ onClose: PropTypes.func.isRequired,
+ show: PropTypes.bool.isRequired,
+ t: PropTypes.func.isRequired,
+ actions: PropTypes.shape({
+ addNotification: PropTypes.func.isRequired,
+ fetchResources: PropTypes.func.isRequired,
+ }),
+ resources: PropTypes.object,
+ state: PropTypes.object,
+ onCancelSuccess: PropTypes.func,
+ fontSize: PropTypes.string.isRequired,
+};
+
+function mapStateToProps(state) {
+ return {
+ currentLanguage: currentLanguageSelector(state),
+ resources: resourcesSelector(state),
+ state,
+ fontSize: fontSizeSelector(state),
+ };
+}
+
+function mapDispatchToProps(dispatch) {
+ const actionCreators = {
+ addNotification,
+ fetchResources,
+ };
+
+ return { actions: bindActionCreators(actionCreators, dispatch) };
+}
+
+const UnconnectedMassCancelModal = injectT(MassCancelModal);
+export { UnconnectedMassCancelModal };
+export default connect(mapStateToProps, mapDispatchToProps)(injectT(MassCancelModal));
diff --git a/app/shared/modals/reservation-mass-cancel/RequiredSpan.js b/app/shared/modals/reservation-mass-cancel/RequiredSpan.js
new file mode 100644
index 000000000..8dce171ea
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/RequiredSpan.js
@@ -0,0 +1,7 @@
+import React from 'react';
+
+function RequiredSpan() {
+ return (* );
+}
+
+export default RequiredSpan;
diff --git a/app/shared/modals/reservation-mass-cancel/_reservation-mass-cancel-modal.scss b/app/shared/modals/reservation-mass-cancel/_reservation-mass-cancel-modal.scss
new file mode 100644
index 000000000..51546573c
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/_reservation-mass-cancel-modal.scss
@@ -0,0 +1,9 @@
+.mass-cancel-date-input {
+ .DayPickerInput {
+ display: block;
+ }
+}
+
+.has-error {
+ color: $varaamo-high-contrast-red;
+}
\ No newline at end of file
diff --git a/app/shared/modals/reservation-mass-cancel/massCancelUtils.js b/app/shared/modals/reservation-mass-cancel/massCancelUtils.js
new file mode 100644
index 000000000..8cc24aa16
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/massCancelUtils.js
@@ -0,0 +1,87 @@
+import { buildAPIUrl, getHeadersCreator } from '../../../utils/apiUtils';
+
+export const FORM_INITIAL_VALUES = {
+ selectedResource: '',
+ startDate: '',
+ endDate: '',
+ confirmCancel: false,
+ cancelling: false,
+ errors: {},
+};
+
+export const FORM_ERRORS = {
+ requiredResource: 'MassCancel.error.required.resource',
+ requiredStart: 'MassCancel.error.required.start',
+ requiredEnd: 'MassCancel.error.required.end',
+ startAfterEnd: 'MassCancel.error.startAfterEnd',
+ endBeforeStart: 'MassCancel.error.endBeforeStart',
+ confirmCancel: 'MassCancel.error.confirmCancel',
+};
+
+/**
+ * Maps resources for select component
+ * @param {Object[]} resources list of resource objects
+ * @returns {Object[]} list of options for select component
+ */
+export function mapResourceOptions(resources) {
+ if (resources) {
+ return resources.map(resource => ({
+ value: resource.id,
+ label: resource.name
+ }));
+ }
+ return [];
+}
+
+/**
+ * Validates form
+ * @param {Object} values form values
+ * @returns {Object} errors
+ */
+export function validateForm(values) {
+ const errors = {};
+ if (!values.selectedResource) {
+ errors.selectedResource = FORM_ERRORS.requiredResource;
+ }
+ if (!values.startDate) {
+ errors.startDate = FORM_ERRORS.requiredStart;
+ }
+ if (!values.endDate) {
+ errors.endDate = FORM_ERRORS.requiredEnd;
+ }
+ if (values.startDate && values.endDate) {
+ const startDate = new Date(values.startDate);
+ const endDate = new Date(values.endDate);
+ if (startDate > endDate || startDate.getTime() === endDate.getTime()) {
+ errors.startDate = FORM_ERRORS.startAfterEnd;
+ errors.endDate = FORM_ERRORS.endBeforeStart;
+ }
+ }
+ if (!values.confirmCancel) {
+ errors.confirmCancel = FORM_ERRORS.confirmCancel;
+ }
+
+ return errors;
+}
+
+/**
+ * Sends mass cancel request to API
+ * @param {string} resourceId resource id
+ * @param {string} begin start datetime
+ * @param {string} end end datetime
+ * @param {Object} state redux state
+ * @returns {Object} object containing errorOccurred bool and code int
+ */
+export async function sendMassCancel(resourceId, begin, end, state) {
+ const apiUrl = buildAPIUrl(`resource/${resourceId}/cancel_reservations`);
+ const payload = { begin, end };
+ const response = await fetch(apiUrl, {
+ method: 'DELETE',
+ headers: getHeadersCreator()(state),
+ body: JSON.stringify(payload),
+ });
+ if (!response.ok) {
+ return { errorOccurred: true, code: response.status };
+ }
+ return { errorOccurred: false, code: response.status };
+}
diff --git a/app/shared/modals/reservation-mass-cancel/tests/MassCancelForm.spec.js b/app/shared/modals/reservation-mass-cancel/tests/MassCancelForm.spec.js
new file mode 100644
index 000000000..1cb634f36
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/tests/MassCancelForm.spec.js
@@ -0,0 +1,147 @@
+import React from 'react';
+
+import Resource from '../../../../utils/fixtures/Resource';
+import { mapResourceOptions } from '../massCancelUtils';
+import MassCancelForm from '../MassCancelForm';
+import { shallowWithIntl } from 'utils/testUtils';
+import SelectField from '../../../../pages/manage-reservations/inputs/SelectField';
+
+
+describe('MassCancelForm', () => {
+ const resourceA = Resource.build();
+ const resourceB = Resource.build();
+ const resources = [resourceA, resourceB];
+ const defaultProps = {
+ resources,
+ selectedResource: '',
+ setSelectedResource: () => {},
+ errors: {},
+ startDate: '',
+ setStartDate: () => {},
+ endDate: '',
+ setEndDate: () => {},
+ confirmCancel: false,
+ setConfirmCancel: () => {},
+ handleOnBlur: () => {},
+ t: () => {},
+ };
+ function getWrapper(extraProps = {}) {
+ return shallowWithIntl( );
+ }
+
+ describe('rendering', () => {
+ test('renders without crashing', () => {
+ getWrapper();
+ });
+
+ test('renders instructions', () => {
+ const wrapper = getWrapper();
+ expect(wrapper.find('p').text()).toEqual('MassCancel.instructions');
+ });
+
+ test('renders resource select field', () => {
+ const select = getWrapper().find(SelectField);
+ expect(select).toHaveLength(1);
+ expect(select.prop('error')).toBe(defaultProps.errors.selectedResource);
+ expect(select.prop('id')).toBe('mass-cancel-resource');
+ expect(select.prop('isRequired')).toBe(true);
+ expect(select.prop('label')).toBe('MassCancel.resourceLabel');
+ expect(select.prop('onChange')).toBeDefined();
+ expect(select.prop('options')).toEqual(mapResourceOptions(resources));
+ expect(select.prop('value')).toBe(defaultProps.selectedResource);
+ });
+
+ test('renders FormGroup for start date', () => {
+ const formGroup = getWrapper().find('FormGroup');
+ expect(formGroup.at(0).prop('controlId')).toBe('mass-cancel-start-date');
+ });
+
+ test('renders ControlLabel for start date', () => {
+ const controlLabel = getWrapper().find('ControlLabel').at(0);
+ expect(controlLabel.children().at(0).text()).toBe('MassCancel.datetime.startLabel');
+
+ const requiredSpan = controlLabel.find('RequiredSpan');
+ expect(requiredSpan).toHaveLength(1);
+ });
+
+ test('renders FormControl for start date', () => {
+ const formControl = getWrapper().find('FormControl').at(0);
+ expect(formControl.prop('aria-describedby')).toBe(defaultProps.errors.startDate ? 'mass-cancel-start-date-error' : null);
+ expect(formControl.prop('aria-required')).toBe('true');
+ expect(formControl.prop('id')).toBe('mass-cancel-start-date');
+ expect(formControl.prop('onBlur')).toBeDefined();
+ expect(formControl.prop('onChange')).toBeDefined();
+ expect(formControl.prop('type')).toBe('datetime-local');
+ expect(formControl.prop('value')).toBe(defaultProps.startDate);
+ });
+
+ describe('start errors', () => {
+ test('renders error message for start date', () => {
+ const wrapper = getWrapper({ errors: { startDate: 'MassCancel.error.required.start' } });
+ const helpBlock = wrapper.find('HelpBlock').at(0);
+ expect(helpBlock).toHaveLength(1);
+ expect(helpBlock.children().at(0).text()).toEqual('MassCancel.error.required.start');
+ expect(helpBlock.prop('id')).toBe('mass-cancel-start-date-error');
+ expect(helpBlock.prop('role')).toBe('alert');
+ expect(helpBlock.prop('className')).toBe('has-error');
+ });
+
+ test('does not render error message for start date if no error', () => {
+ const wrapper = getWrapper({ errors: { startDate: null } });
+ const helpBlock = wrapper.find('HelpBlock').at(0);
+ expect(helpBlock).toHaveLength(0);
+ });
+ });
+
+ test('renders FormGroup for end date', () => {
+ const formGroup = getWrapper().find('FormGroup');
+ expect(formGroup.at(1).prop('controlId')).toBe('mass-cancel-end-date');
+ });
+
+ test('renders ControlLabel for end date', () => {
+ const controlLabel = getWrapper().find('ControlLabel').at(1);
+ expect(controlLabel.children().at(0).text()).toBe('MassCancel.datetime.endLabel');
+
+ const requiredSpan = controlLabel.find('RequiredSpan');
+ expect(requiredSpan).toHaveLength(1);
+ });
+
+ test('renders FormControl for end date', () => {
+ const formControl = getWrapper().find('FormControl').at(1);
+ expect(formControl.prop('aria-describedby')).toBe(defaultProps.errors.endDate ? 'mass-cancel-end-date-error' : null);
+ expect(formControl.prop('aria-required')).toBe('true');
+ expect(formControl.prop('id')).toBe('mass-cancel-end-date');
+ expect(formControl.prop('onBlur')).toBeDefined();
+ expect(formControl.prop('onChange')).toBeDefined();
+ expect(formControl.prop('type')).toBe('datetime-local');
+ expect(formControl.prop('value')).toBe(defaultProps.endDate);
+ });
+
+ describe('end errors', () => {
+ test('renders error message for end date', () => {
+ const wrapper = getWrapper({ errors: { endDate: 'MassCancel.error.required.end' } });
+ const helpBlock = wrapper.find('HelpBlock').at(0);
+ expect(helpBlock).toHaveLength(1);
+ expect(helpBlock.children().at(0).text()).toEqual('MassCancel.error.required.end');
+ expect(helpBlock.prop('id')).toBe('mass-cancel-end-date-error');
+ expect(helpBlock.prop('role')).toBe('alert');
+ expect(helpBlock.prop('className')).toBe('has-error');
+ });
+
+ test('does not render error message for end date if no error', () => {
+ const wrapper = getWrapper({ errors: { endDate: null } });
+ const helpBlock = wrapper.find('HelpBlock').at(0);
+ expect(helpBlock).toHaveLength(0);
+ });
+ });
+
+ test('renders checkbox for confirming cancellation', () => {
+ const checkbox = getWrapper().find('Checkbox');
+ expect(checkbox.prop('aria-required')).toBe('true');
+ expect(checkbox.prop('checked')).toBe(defaultProps.confirmCancel);
+ expect(checkbox.prop('onBlur')).toBeDefined();
+ expect(checkbox.prop('onChange')).toBeDefined();
+ expect(checkbox.children().at(0).text()).toBe('MassCancel.confirmCancel');
+ });
+ });
+});
diff --git a/app/shared/modals/reservation-mass-cancel/tests/MassCancelModal.spec.js b/app/shared/modals/reservation-mass-cancel/tests/MassCancelModal.spec.js
new file mode 100644
index 000000000..b0f68ed77
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/tests/MassCancelModal.spec.js
@@ -0,0 +1,101 @@
+import React from 'react';
+import { Modal } from 'react-bootstrap';
+
+import { shallowWithIntl } from 'utils/testUtils';
+import Resource from '../../../../utils/fixtures/Resource';
+import { UnconnectedMassCancelModal as MassCancelModal } from '../MassCancelModal';
+import MassCancelForm from '../MassCancelForm';
+import { FORM_INITIAL_VALUES } from '../massCancelUtils';
+
+
+describe('MassCancelForm', () => {
+ const resourceA = Resource.build();
+ const resourceB = Resource.build();
+ const resources = { [resourceA.id]: resourceA, [resourceB.id]: resourceB };
+ const defaultProps = {
+ resources,
+ show: true,
+ fontSize: '__font-size-small',
+ onCancelSuccess: jest.fn(),
+ onClose: jest.fn(),
+ t: () => {},
+ actions: {
+ addNotification: jest.fn(),
+ fetchResources: jest.fn(),
+ },
+ state: {},
+ };
+
+ function getWrapper(extraProps = {}) {
+ return shallowWithIntl( );
+ }
+
+ describe('rendering', () => {
+ test('renders a Modal', () => {
+ const modal = getWrapper().find(Modal);
+ expect(modal).toHaveLength(1);
+ expect(modal.prop('show')).toBe(true);
+ expect(modal.prop('className')).toBe(defaultProps.fontSize);
+ expect(modal.prop('onHide')).toBeDefined();
+ expect(modal.prop('animation')).toBe(false);
+ });
+
+ test('renders a Modal.Header', () => {
+ const header = getWrapper().find(Modal.Header);
+ expect(header).toHaveLength(1);
+ expect(header.prop('closeButton')).toBe(true);
+ });
+
+ test('renders a Modal.Title', () => {
+ const title = getWrapper().find(Modal.Title);
+ expect(title).toHaveLength(1);
+ expect(title.children().text()).toBe('common.cancelReservations');
+ expect(title.prop('componentClass')).toBe('h2');
+ });
+
+ test('renders a Modal.Body', () => {
+ const body = getWrapper().find(Modal.Body);
+ expect(body).toHaveLength(1);
+ });
+
+ test('renders a MassCancelForm', () => {
+ const form = getWrapper().find(MassCancelForm);
+ expect(form).toHaveLength(1);
+ expect(form.prop('resources')).toStrictEqual(Object.values(defaultProps.resources));
+ expect(form.prop('selectedResource')).toBe(FORM_INITIAL_VALUES.selectedResource);
+ expect(form.prop('confirmCancel')).toBe(FORM_INITIAL_VALUES.confirmCancel);
+ expect(form.prop('endDate')).toBe(FORM_INITIAL_VALUES.endDate);
+ expect(form.prop('errors')).toBe(FORM_INITIAL_VALUES.errors);
+ expect(form.prop('setConfirmCancel')).toBeDefined();
+ expect(form.prop('setEndDate')).toBeDefined();
+ expect(form.prop('setSelectedResource')).toBeDefined();
+ expect(form.prop('setStartDate')).toBeDefined();
+ expect(form.prop('startDate')).toBe(FORM_INITIAL_VALUES.startDate);
+ expect(form.prop('handleOnBlur')).toBeDefined();
+ });
+
+ test('renders a Modal.Footer', () => {
+ const footer = getWrapper().find(Modal.Footer);
+ expect(footer).toHaveLength(1);
+ });
+
+ test('renders a back button', () => {
+ const backButton = getWrapper().find('Button').at(0);
+ expect(backButton).toHaveLength(1);
+ expect(backButton.prop('bsStyle')).toBe('primary');
+ expect(backButton.prop('className')).toBe(defaultProps.fontSize);
+ expect(backButton.prop('onClick')).toBeDefined();
+ expect(backButton.children().text()).toBe('common.back');
+ });
+
+ test('renders a cancel reservations button', () => {
+ const cancelButton = getWrapper().find('Button').at(1);
+ expect(cancelButton).toHaveLength(1);
+ expect(cancelButton.prop('bsStyle')).toBe('danger');
+ expect(cancelButton.prop('className')).toBe(defaultProps.fontSize);
+ expect(cancelButton.prop('disabled')).toBe(true);
+ expect(cancelButton.prop('onClick')).toBeDefined();
+ expect(cancelButton.children().text()).toBe('MassCancel.cancelReservations');
+ });
+ });
+});
diff --git a/app/shared/modals/reservation-mass-cancel/tests/RequiredSpan.spec.js b/app/shared/modals/reservation-mass-cancel/tests/RequiredSpan.spec.js
new file mode 100644
index 000000000..e34c06a98
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/tests/RequiredSpan.spec.js
@@ -0,0 +1,17 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import RequiredSpan from '../RequiredSpan';
+
+describe('RequiredSpan', () => {
+ function getWrapper() {
+ return shallow( );
+ }
+
+ test('renders correctly', () => {
+ const span = getWrapper();
+ expect(span.length).toBe(1);
+ expect(span.prop('aria-hidden')).toBe('true');
+ expect(span.text()).toBe('*');
+ });
+});
diff --git a/app/shared/modals/reservation-mass-cancel/tests/massCancelUtils.spec.js b/app/shared/modals/reservation-mass-cancel/tests/massCancelUtils.spec.js
new file mode 100644
index 000000000..9c437b826
--- /dev/null
+++ b/app/shared/modals/reservation-mass-cancel/tests/massCancelUtils.spec.js
@@ -0,0 +1,102 @@
+import {
+ FORM_ERRORS, mapResourceOptions, sendMassCancel, validateForm
+} from '../massCancelUtils';
+
+describe('massCancelUtils', () => {
+ describe('mapResourceOptions', () => {
+ test('returns an empty array if resources is undefined', () => {
+ expect(mapResourceOptions(undefined)).toEqual([]);
+ });
+
+ test('returns an empty array if resources is null', () => {
+ expect(mapResourceOptions(null)).toEqual([]);
+ });
+
+ test('returns an array of objects with the correct properties', () => {
+ const resources = [{ id: 1, name: 'Resource 1' }, { id: 2, name: 'Resource 2' }];
+ const expectedOutput = [
+ { value: 1, label: 'Resource 1' },
+ { value: 2, label: 'Resource 2' }
+ ];
+ expect(mapResourceOptions(resources)).toEqual(expectedOutput);
+ });
+ });
+
+ describe('validateForm', () => {
+ test('returns an empty object if all fields are valid', () => {
+ const values = {
+ selectedResource: 1,
+ startDate: '2020-01-01',
+ endDate: '2020-01-02',
+ confirmCancel: true
+ };
+ expect(validateForm(values)).toEqual({});
+ });
+ test('returns an object with errors if any fields are invalid', () => {
+ const values = {
+ selectedResource: undefined,
+ startDate: undefined,
+ endDate: undefined,
+ confirmCancel: undefined
+ };
+ expect(validateForm(values)).toEqual({
+ selectedResource: FORM_ERRORS.requiredResource,
+ startDate: FORM_ERRORS.requiredStart,
+ endDate: FORM_ERRORS.requiredEnd,
+ confirmCancel: FORM_ERRORS.confirmCancel
+ });
+
+ const values2 = {
+ selectedResource: 1,
+ startDate: '2020-01-01',
+ endDate: '2020-01-01',
+ confirmCancel: undefined
+ };
+ expect(validateForm(values2)).toEqual({
+ confirmCancel: FORM_ERRORS.confirmCancel,
+ endDate: FORM_ERRORS.endBeforeStart,
+ startDate: FORM_ERRORS.startAfterEnd
+ });
+ });
+ });
+
+ describe('sendMassCancel', () => {
+ test('returns an object with errorOccurred set to true if an error occurs', () => {
+ global.fetch = jest.fn(() => Promise.resolve({
+ ok: false,
+ status: 500,
+ }));
+ const resourceId = 1;
+ const begin = '2020-01-01';
+ const end = '2020-01-02';
+ const state = {
+ auth: {
+ user: {
+ id_token: 'token'
+ }
+ }
+ };
+ const expectedOutput = { errorOccurred: true, code: 500 };
+ expect(sendMassCancel(resourceId, begin, end, state)).resolves.toEqual(expectedOutput);
+ });
+
+ test('returns an object with errorOccurred set to false if no error occurs', () => {
+ global.fetch = jest.fn(() => Promise.resolve({
+ ok: true,
+ status: 200,
+ }));
+ const resourceId = 1;
+ const begin = '2020-01-01';
+ const end = '2020-01-02';
+ const state = {
+ auth: {
+ user: {
+ id_token: 'token'
+ }
+ }
+ };
+ const expectedOutput = { errorOccurred: false, code: 200 };
+ expect(sendMassCancel(resourceId, begin, end, state)).resolves.toEqual(expectedOutput);
+ });
+ });
+});