Skip to content

Commit

Permalink
Added a timeslot notification for maintenance mode (#267)
Browse files Browse the repository at this point in the history
Changes:
- added custom error notification for timeslot selecting when maintenance mode is on
- added redux state handling for maintenance mode reading and writing
  • Loading branch information
SanttuA authored Sep 14, 2023
1 parent 4c89849 commit bd8c3ec
Show file tree
Hide file tree
Showing 18 changed files with 149 additions and 8 deletions.
5 changes: 5 additions & 0 deletions app/actions/announcementActions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createAction } from 'redux-actions';

import types from 'constants/ActionTypes';

export const setMaintenanceMode = createAction(types.UI.SET_MAINTENANCE_MODE);
1 change: 1 addition & 0 deletions app/constants/ActionTypes.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ export default {
SELECT_RESERVATION_TO_CANCEL: 'SELECT_RESERVATION_TO_CANCEL',
SELECT_RESERVATION_TO_EDIT: 'SELECT_RESERVATION_TO_EDIT',
SELECT_RESERVATION_TO_SHOW: 'SELECT_RESERVATION_TO_SHOW',
SET_MAINTENANCE_MODE: 'SET_MAINTENANCE_MODE',
SHOW_RESERVATION_INFO_MODAL: 'SHOW_RESERVATION_INFO_MODAL',
START_RESERVATION_EDIT_IN_INFO_MODAL: 'START_RESERVATION_EDIT_IN_INFO_MODAL',
TOGGLE_RESOURCE_SHOW_MAP: 'TOGGLE_RESOURCE_SHOW_MAP',
Expand Down
1 change: 1 addition & 0 deletions app/i18n/messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
"NotFoundPage.searchPage": "If you are looking for a certain space, you can search for it through {searchPageLink}.",
"NotFoundPage.searchPageLink": "search page",
"NotFoundPage.title": "404 Page not found",
"Notifications.cannotReserveDuringMaintenance": "Cannot make reservations during maintenance.",
"Notifications.errorMessage": "Something went wrong. Please try again in a moment.",
"Notifications.loginErrorMessage": "Failed to login. Please try again in a moment.",
"Notifications.userFetchErrorMessage": "Failed to retrieve user info. Please try again in a moment.",
Expand Down
1 change: 1 addition & 0 deletions app/i18n/messages/fi.json
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,7 @@
"NotFoundPage.searchPage": "Jos etsit jotain tiettyä tilaa, voit etsiä sitä {searchPageLink}.",
"NotFoundPage.searchPageLink": "hakusivulta",
"NotFoundPage.title": "404 Sivua ei löydy",
"Notifications.cannotReserveDuringMaintenance": "Huoltotyön aikana ei voi tehdä varauksia.",
"Notifications.errorMessage": "Jotain meni vikaan. Yritä hetken päästä uudelleen.",
"Notifications.loginErrorMessage": "Kirjautuminen epäonnistui. Yritä hetken päästä uudelleen.",
"Notifications.userFetchErrorMessage": "Käyttäjätietojen hakeminen epäonnistui. Yritä hetken päästä uudelleen.",
Expand Down
1 change: 1 addition & 0 deletions app/i18n/messages/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@
"NotFoundPage.searchPage": "Om du letar efter ett särskilt utrymme, kan du använda dig av söksidan {searchPageLink}.",
"NotFoundPage.searchPageLink": "söksidan",
"NotFoundPage.title": "404 Webbplatsen hittades inte",
"Notifications.cannotReserveDuringMaintenance": "Det går inte att göra reservationer under underhåll.",
"Notifications.errorMessage": "Ett fel uppstod. Försök på nytt om en liten stund.",
"Notifications.loginErrorMessage": "Inloggning misslyckades. Försök igen om en liten stund.",
"Notifications.userFetchErrorMessage": "Användarinformations hämtning misslyckades. Försök igen om en liten stund.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export class UnconnectedReservationCalendarContainer extends Component {
t: PropTypes.func.isRequired,
time: PropTypes.string,
timeSlots: PropTypes.array.isRequired,
isMaintenanceModeOn: PropTypes.bool.isRequired,
};

getSelectedDateSlots = (timeSlots, selected) => {
Expand Down Expand Up @@ -148,6 +149,7 @@ export class UnconnectedReservationCalendarContainer extends Component {
t,
time,
timeSlots,
isMaintenanceModeOn,
} = this.props;

const isOpen = Boolean(timeSlots.length);
Expand All @@ -164,6 +166,7 @@ export class UnconnectedReservationCalendarContainer extends Component {
isEditing={isEditing}
isFetching={isFetchingResource}
isLoggedIn={isLoggedIn || resource.authentication === 'unauthenticated'} // count as logged in if no auth needed
isMaintenanceModeOn={isMaintenanceModeOn}
isStaff={isStaff}
isStrongAuthSatisfied={isStrongAuthSatisfied}
onClear={actions.clearTimeSlots}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ describe('pages/resource/reservation-calendar/ReservationCalendarContainer', ()
resource,
selected: [],
timeSlots: [[TimeSlot.build()], [TimeSlot.build()]],
isMaintenanceModeOn: false,
};
function getWrapper(props) {
return shallowWithIntl(<ReservationCalendarContainer {...defaultProps} {...props} />);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const resourceSelector = createResourceSelector(resourceIdSelector);
const selectedSelector = state => state.ui.reservations.selected;
const selectedReservationSlotSelector = state => state.ui.reservations.selectedSlot;
const toEditSelector = state => state.ui.reservations.toEdit;
const isMaintenanceModeOnSelector = state => state.ui.maintenance.isMaintenanceModeOn;

const isEditingSelector = createSelector(
toEditSelector,
Expand Down Expand Up @@ -107,6 +108,7 @@ const reservationCalendarSelector = createStructuredSelector({
selectedReservationSlot: selectedReservationSlotSelector,
time: timeSelector,
timeSlots: timeSlotsSelector,
isMaintenanceModeOn: isMaintenanceModeOnSelector,
});

export default reservationCalendarSelector;
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ function getState(resource) {
selectedSlot: { foo: 'bar' },
toEdit: ['mock-reservation'],
},
maintenance: {
isMaintenanceModeOn: false,
},
}),
};
}
Expand Down Expand Up @@ -108,6 +111,14 @@ describe('pages/resource/reservation-calendar/reservationCalendarSelector', () =
expect(selected.isStrongAuthSatisfied).toBeDefined();
});

test('returns isMaintenanceModeOn', () => {
const state = getState(resource);
const props = getProps(resource.id);
const selected = reservationCalendarSelector(state, props);

expect(selected.isMaintenanceModeOn).toBeDefined();
});

test('returns isStaff', () => {
const state = getState(resource);
const props = getProps(resource.id);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class TimeSlot extends PureComponent {
selected: PropTypes.bool.isRequired,
slot: PropTypes.object.isRequired,
t: PropTypes.func.isRequired,
isMaintenanceModeOn: PropTypes.bool.isRequired,
};

static defaultProps = {
Expand Down Expand Up @@ -134,10 +135,18 @@ class TimeSlot extends PureComponent {
}

getReservationInfoNotification(isLoggedIn, isStrongAuthSatisfied, resource, slot, t) {
const { isMaintenanceModeOn } = this.props;
if (new Date(slot.end) < new Date() || slot.reserved) {
return null;
}

if (isMaintenanceModeOn) {
return {
message: t('Notifications.cannotReserveDuringMaintenance'),
type: 'info',
timeOut: 10000,
};
}
if (resource.reservable && !isStrongAuthSatisfied) {
return {
message: t('Notifications.loginToReserveStrongAuth'),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlot', () => {
selected: false,
showClear: false,
slot: Immutable(TimeSlotFixture.build()),
isMaintenanceModeOn: false,
};

function getWrapper(extraProps) {
Expand Down Expand Up @@ -236,6 +237,21 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlot', () => {
expect(t.callCount).toBe(0);
});

test('returns correct message when maintenance mode is on', () => {
const t = message => message;
const isMaintenanceModeOn = true;
const isStrongAuthSatisfied = false;
const resource = Resource.build({ reservable: true });
const instance = getWrapper({ isMaintenanceModeOn }).instance();
const result = instance.getReservationInfoNotification(
true, isStrongAuthSatisfied, resource, defaultProps.slot, t
);

expect(result.message).toBe(t('Notifications.cannotReserveDuringMaintenance'));
expect(result.type).toBe('info');
expect(result.timeOut).toBe(10000);
});

test('returns correct message when resource is reservable but not strong auth satisfied', () => {
const t = message => message;
const resource = Resource.build({ reservable: true });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ class TimeSlots extends Component {
slots: PropTypes.array.isRequired,
t: PropTypes.func.isRequired,
time: PropTypes.string,
isMaintenanceModeOn: PropTypes.bool.isRequired,
};

state = {
Expand Down Expand Up @@ -219,6 +220,7 @@ class TimeSlots extends Component {
selected,
t,
time,
isMaintenanceModeOn,
} = this.props;
const { hoveredTimeSlot } = this.state;
if (!slot.end) {
Expand Down Expand Up @@ -264,6 +266,7 @@ class TimeSlots extends Component {
isEditing={isEditing}
isHighlighted={isHighlighted}
isLoggedIn={isLoggedIn}
isMaintenanceModeOn={isMaintenanceModeOn}
isSelectable={isSelectable}
isStrongAuthSatisfied={isStrongAuthSatisfied}
isUnderMinPeriod={isUnderMinPeriod}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlots', () => {
],
selectedDate: '2016-10-10',
slots: Immutable(defaultSlots),
isMaintenanceModeOn: false,
};

function getWrapper(props) {
Expand Down Expand Up @@ -152,6 +153,7 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlots', () => {
expect(timeSlot.props().onClick).toBe(defaultProps.onClick);
expect(timeSlot.props().resource).toEqual(defaultProps.resource);
expect(timeSlot.props().slot).toEqual(defaultProps.slots[index][0]);
expect(timeSlot.props().isMaintenanceModeOn).toEqual(defaultProps.isMaintenanceModeOn);
});
});

Expand Down
29 changes: 24 additions & 5 deletions app/shared/service-announcement/ServiceAnnouncement.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import React from 'react';
import Alert from 'react-bootstrap/lib/Alert';
import Button from 'react-bootstrap/lib/Button';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';

import { injectT } from 'i18n';
import { fetchAnnouncement } from './serviceAnnouncementUtils';
import { getLocalizedFieldValue } from '../../utils/languageUtils';
import { setMaintenanceMode } from '../../actions/announcementActions';

class ServiceAnnouncement extends React.Component {
constructor(props) {
Expand All @@ -20,10 +23,15 @@ class ServiceAnnouncement extends React.Component {

componentDidMount() {
fetchAnnouncement()
.then(data => this.setState({
announcement: (data.results && data.results[0]) ? data.results[0] : null,
show: data.results && data.results.length > 0
}))
.then(data => {
this.setState({
announcement: (data.results && data.results[0]) ? data.results[0] : null,
show: data.results && data.results.length > 0
});
this.props.actions.setMaintenanceMode((data.results && data.results[0])
? data.results[0].is_maintenance_mode_on : false
);
})
.catch(() => this.setState({ announcement: null, show: false }));
}

Expand Down Expand Up @@ -68,9 +76,20 @@ class ServiceAnnouncement extends React.Component {
}

ServiceAnnouncement.propTypes = {
actions: PropTypes.object.isRequired,
contrast: PropTypes.string.isRequired,
currentLanguage: PropTypes.string.isRequired,
t: PropTypes.func.isRequired,
};

export default injectT(ServiceAnnouncement);
function mapDispatchToProps(dispatch) {
const actionCreators = {
setMaintenanceMode
};

return { actions: bindActionCreators(actionCreators, dispatch) };
}

export const ServiceAnnouncementWithT = injectT(ServiceAnnouncement);

export default connect(null, mapDispatchToProps)(ServiceAnnouncementWithT);
14 changes: 11 additions & 3 deletions app/shared/service-announcement/tests/ServiceAnnouncement.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Button } from 'react-bootstrap';

import { shallowWithIntl } from 'utils/testUtils';
import { getLocalizedFieldValue } from '../../../utils/languageUtils';
import ServiceAnnouncement from '../ServiceAnnouncement';
import { ServiceAnnouncementWithT as ServiceAnnouncement } from '../ServiceAnnouncement';
import { fetchAnnouncement } from '../serviceAnnouncementUtils';

jest.mock('../serviceAnnouncementUtils', () => {
Expand All @@ -12,15 +12,16 @@ jest.mock('../serviceAnnouncementUtils', () => {
__esModule: true,
...originalModule,
fetchAnnouncement: jest.fn(() => Promise.resolve({
data: { results: [{ message: { fi: 'test' } }] },
data: { results: [{ message: { fi: 'test' }, is_maintenance_mode_on: true }] },
})),
};
});

describe('shared/service-announcement/ServiceAnnouncement', () => {
const defaultProps = {
contrast: '',
currentLanguage: 'fi'
currentLanguage: 'fi',
actions: { setMaintenanceMode: jest.fn() }
};

function getWrapper(extraProps) {
Expand Down Expand Up @@ -106,6 +107,7 @@ describe('shared/service-announcement/ServiceAnnouncement', () => {
describe('componentDidMount', () => {
afterEach(() => {
fetchAnnouncement.mockClear();
defaultProps.actions.setMaintenanceMode.mockClear();
});

test('calls fetchAnnouncement and setState', async () => {
Expand All @@ -115,6 +117,12 @@ describe('shared/service-announcement/ServiceAnnouncement', () => {
expect(fetchAnnouncement).toHaveBeenCalledTimes(1);
expect(spy).toHaveBeenCalledTimes(1);
});

test('calls props.actions.setMaintenanceMode', async () => {
const instance = getWrapper().instance();
await instance.componentDidMount();
expect(defaultProps.actions.setMaintenanceMode).toHaveBeenCalledTimes(1);
});
});
});
});
2 changes: 2 additions & 0 deletions app/state/reducers/ui/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import searchReducer from './searchReducer';
import accessibilityReducer from './accessibilityReducer';
import favoritesPagesReducer from './favoritesPageReducer';
import manageReservationsPageReducer from './manageReservationsPageReducer';
import maintenanceReducer from './maintenanceReducer';

const uiReducers = combineReducers({
modals: modalsReducer,
Expand All @@ -24,6 +25,7 @@ const uiReducers = combineReducers({
resourceMap: resourceMapReducer,
search: searchReducer,
accessibility: accessibilityReducer,
maintenance: maintenanceReducer,
});

export default uiReducers;
23 changes: 23 additions & 0 deletions app/state/reducers/ui/maintenanceReducer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import Immutable from 'seamless-immutable';

import types from 'constants/ActionTypes';

const initialState = Immutable({
isMaintenanceModeOn: false,
});

function maintenanceReducer(state = initialState, action) {
switch (action.type) {
case types.UI.SET_MAINTENANCE_MODE: {
return state.merge({
isMaintenanceModeOn: action.payload
});
}

default: {
return state;
}
}
}

export default maintenanceReducer;
33 changes: 33 additions & 0 deletions app/state/reducers/ui/maintenanceReducer.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@

import Immutable from 'seamless-immutable';

import types from 'constants/ActionTypes';
import reducer from './maintenanceReducer';

describe('/state/reducers/ui/maintenanceReducer', () => {
const initialState = Immutable({
isMaintenanceModeOn: false
});

const action = {
type: types.UI.SET_MAINTENANCE_MODE,
payload: true
};

const expectedState = { isMaintenanceModeOn: true };

test('initialState is correct', () => {
const actual = reducer(undefined, { type: 'nothing' });
expect(actual).toEqual(initialState);
});

test('returns correct state on SET_MAINTENANCE_MODE', () => {
const actual = reducer(undefined, action);
expect(actual).toEqual(expectedState);
});

test('returns correct state if no matching action type is given', () => {
const actual = reducer(undefined, { type: types.API.RESOURCES_GET_SUCCESS });
expect(actual).toEqual(initialState);
});
});

0 comments on commit bd8c3ec

Please sign in to comment.