diff --git a/CHANGELOG.md b/CHANGELOG.md
index a6b7b84ff..0d77b9a20 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,16 @@
+* 0.1.2
+ ** MAJOR CHANGES **
+
+ - Add new selection field to sort filtered resources. Currently support to search by name, type, premise, people.
+ - Temporarily only show warning messages in 3 languages for IE11 user.
+ - Ability to favourite resources straight on search view instead going to resource detail page.
+
+ ** CHANGELOG **
+
+ - [#895](https://github.com/City-of-Helsinki/varaamo/pull/895) Add sort to sort filtered resources.
+ - [#904](https://github.com/City-of-Helsinki/varaamo/pull/904) Favourite Resource on search view.
+ - [#909](https://github.com/City-of-Helsinki/varaamo/pull/909) Show warning message for IE11 users.
+
* 0.1.1
** HOTFIX **
@@ -13,70 +26,70 @@
** CHANGELOG **
UI changes:
- - #863: Make homepage banner clickable.
+ - [#863](https://github.com/City-of-Helsinki/varaamo/pull/863): Make homepage banner clickable.
- - #865: Delayed reservation.
+ - [#865](https://github.com/City-of-Helsinki/varaamo/pull/865): Delayed reservation.
- - #867: Add config to fetch all unit that doesn't have empty resources.
+ - [#867](https://github.com/City-of-Helsinki/varaamo/pull/867): Add config to fetch all unit that doesn't have empty resources.
- - #868: Add municipality filters for filtering resources base on municiples.
+ - [#868](https://github.com/City-of-Helsinki/varaamo/pull/868): Add municipality filters for filtering resources base on municiples.
- - #873: Limit the selection of time slots to the ones within max period.
+ - [#873](https://github.com/City-of-Helsinki/varaamo/pull/873): Limit the selection of time slots to the ones within max period.
- - #875: Expand advanced search panel when filters are applied.
+ - [#875](https://github.com/City-of-Helsinki/varaamo/pull/875): Expand advanced search panel when filters are applied.
- - #876: Free-of-charge filter for resources.
+ - [#876](https://github.com/City-of-Helsinki/varaamo/pull/876): Free-of-charge filter for resources.
- - #878: Remove the link for old website from the footer.
+ - [#878](https://github.com/City-of-Helsinki/varaamo/pull/878): Remove the link for old website from the footer.
- - #883: Clear all filters after reset.
+ - [#883](https://github.com/City-of-Helsinki/varaamo/pull/883): Clear all filters after reset.
- - #889: Disable reservation time limit for admins.
+ - [#889](https://github.com/City-of-Helsinki/varaamo/pull/889): Disable reservation time limit for admins.
- - #899: Add unpublished tag to resource search list.
+ - [#899](https://github.com/City-of-Helsinki/varaamo/pull/899): Add unpublished tag to resource search list.
- - #901: Add navigation links to staff and higher permission user.
+ - [#901](https://github.com/City-of-Helsinki/varaamo/pull/901): Add navigation links to staff and higher permission user.
Upgrading:
- - #854: Upgrade react-router to react-router v4.
+ - [#854](https://github.com/City-of-Helsinki/varaamo/pull/854): Upgrade react-router to react-router v4.
- - #856: Add dockerize config to dockerize development environment.
+ - [#856](https://github.com/City-of-Helsinki/varaamo/pull/856): Add dockerize config to dockerize development environment.
- - #857: Upgrade moment, moment-range, moment-timezome.
+ - [#857](https://github.com/City-of-Helsinki/varaamo/pull/857): Upgrade moment, moment-range, moment-timezome.
- - #860: Upgrade lodash.
+ - [#860](https://github.com/City-of-Helsinki/varaamo/pull/860): Upgrade lodash.
- - #862: Replace redux-logger with redux-devtools.
+ - [#862](https://github.com/City-of-Helsinki/varaamo/pull/862): Replace redux-logger with redux-devtools.
- - #868: Upgrade react-select.
+ - [#868](https://github.com/City-of-Helsinki/varaamo/pull/868): Upgrade react-select.
- - #874: Replace React internal prop-types with npm prop-types.
+ - [#874](https://github.com/City-of-Helsinki/varaamo/pull/874): Replace React internal prop-types with npm prop-types.
- - #879: Upgrade React to 15.6.2, Enzyme to v3+.
+ - [#879](https://github.com/City-of-Helsinki/varaamo/pull/879): Upgrade React to 15.6.2, Enzyme to v3+.
- - #882: Upgrade react-day-picker, remove react-date-picker.
+ - [#882](https://github.com/City-of-Helsinki/varaamo/pull/882): Upgrade react-day-picker, remove react-date-picker.
- - #884: Upgrade babel to v7, webpack v4, replace Karma/Mocha/Chai with Jest.
+ - [#884](https://github.com/City-of-Helsinki/varaamo/pull/884): Upgrade babel to v7, webpack v4, replace Karma/Mocha/Chai with Jest.
- - #890: Replace Chai with Jest's assertions.
+ - [#890](https://github.com/City-of-Helsinki/varaamo/pull/890): Replace Chai with Jest's assertions.
- - #892: Remove unnecessary outdated dependencies:
+ - [#892](https://github.com/City-of-Helsinki/varaamo/pull/892): Remove unnecessary outdated dependencies:
- Remove react-document-title, use react-helmet
- Remove react-body-classname, classname append can be handled by classnames
- - #893: Remove unnessary persisted state library, upgrade redux and dependencies:
+ - [#893](https://github.com/City-of-Helsinki/varaamo/pull/893): Remove unnessary persisted state library, upgrade redux and dependencies:
- Remove redux-localstorage and redux-localstorage-filter. Replaced with vanilla code.
- Upgrade redux and dependencies.
- - #894: Upgrade React to 16.8.x:
+ - [#894](https://github.com/City-of-Helsinki/varaamo/pull/894): Upgrade React to 16.8.x:
- Upgrade React to 16.8.x
- Upgrade React's dependencies to latest.
- - #900: Clean up obsolete/deprecated component.
+ - [#900](https://github.com/City-of-Helsinki/varaamo/pull/900): Clean up obsolete/deprecated component.
- Delete navbar, sidebar, side-navbar which was replaced by new component but not getting removed.
diff --git a/app/assets/icons/heart-filled.svg b/app/assets/icons/heart-filled.svg
new file mode 100644
index 000000000..ec29d0177
--- /dev/null
+++ b/app/assets/icons/heart-filled.svg
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/constants/AppConstants.js b/app/constants/AppConstants.js
index caed2752f..d8c11f3ea 100644
--- a/app/constants/AppConstants.js
+++ b/app/constants/AppConstants.js
@@ -57,6 +57,7 @@ export default {
end: '',
lat: '',
lon: '',
+ orderBy: '',
page: 1,
people: '',
purpose: '',
@@ -68,4 +69,11 @@ export default {
TIME_FORMAT: 'H:mm',
TIME_SLOT_DEFAULT_LENGTH: 30,
TRACKING: SETTINGS.TRACKING,
+ SORT_BY_OPTIONS: {
+ NAME: 'resource_name_lang',
+ TYPE: 'type_name_lang',
+ PREMISES: 'unit_name_lang',
+ PEOPLE: 'people_capacity',
+ // TODO: sortby 'open now' should be implemented later after API support it
+ }
};
diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json
index 47bffbe89..9f505c9a3 100644
--- a/app/i18n/messages/en.json
+++ b/app/i18n/messages/en.json
@@ -257,6 +257,11 @@
"SearchControlsContainer.unitLabel": "Premise",
"SearchPage.title": "Search",
"ShowResourcesLink.text": "Show all premises and equipment",
+ "SortBy.label": "Sort By:",
+ "SortBy.name.label": "Name",
+ "SortBy.type.label": "Type",
+ "SortBy.premise.label": "Premise",
+ "SortBy.people.label": "People",
"TestSiteMessage.text": "This is the test version of Varaamo",
"TimeRangeControl.timeRangeTitle": "Time range and minimum duration",
"TimeRangeControl.title": "{date} at {start}-{end} {hours}h booking",
@@ -279,4 +284,4 @@
"UserReservationsPage.regularEmptyMessage": "No standard reservations",
"UserReservationsPage.regularReservationsHeader": "Standard reservations",
"UserReservationsPage.title": "My reservations"
-}
\ No newline at end of file
+}
diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json
index e5daed2bc..34a6981eb 100644
--- a/app/i18n/messages/fi.json
+++ b/app/i18n/messages/fi.json
@@ -257,6 +257,11 @@
"SearchControlsContainer.unitLabel": "Toimipiste",
"SearchPage.title": "Haku",
"ShowResourcesLink.text": "Näytä kaikki tilat ja laitteet",
+ "SortBy.label": "Järjestä:",
+ "SortBy.name.label": "Nimi",
+ "SortBy.type.label": "Tyyppi",
+ "SortBy.premise.label": "Toimipiste",
+ "SortBy.people.label": "Henkilömäärä",
"TimeRangeControl.timeRangeTitle": "Käytä aikaväliä ja varauksen minimipituutta",
"TimeRangeControl.title": "{date} klo {start}-{end} {hours}h varaus",
"TestSiteMessage.text": "Tämä on Varaamon testiversio",
@@ -279,4 +284,4 @@
"UserReservationsPage.regularEmptyMessage": "Ei tavallisia varauksia näytettäväksi.",
"UserReservationsPage.regularReservationsHeader": "Tavalliset varaukset",
"UserReservationsPage.title": "Omat varaukset"
-}
\ No newline at end of file
+}
diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json
index a719e0f34..5e018021c 100644
--- a/app/i18n/messages/sv.json
+++ b/app/i18n/messages/sv.json
@@ -259,6 +259,11 @@
"SearchControlsContainer.unitLabel": "Utrymmet",
"SearchPage.title": "Sök",
"ShowResourcesLink.text": "Visa alla utrymmen och apparater",
+ "SortBy.label": "Sortera efter:",
+ "SortBy.name.label": "Namn",
+ "SortBy.type.label": "Typ",
+ "SortBy.premise.label": "Lokal",
+ "SortBy.people.label": "Antal personer",
"TestSiteMessage.text": "Detta är Varaamo testversion",
"TimeRangeControl.timeRangeTitle": "Tidsurval och minsta reserveringstid",
"TimeRangeControl.title": "{date} kl. {start}-{end} {hours}h bokning",
@@ -281,4 +286,4 @@
"UserReservationsPage.regularEmptyMessage": "Det finns inga vanliga bokningar att visa.",
"UserReservationsPage.regularReservationsHeader": "Vanliga bokningar",
"UserReservationsPage.title": "Mina bokningar"
-}
\ No newline at end of file
+}
diff --git a/app/index.js b/app/index.js
index 63b99124e..18ad32e50 100644
--- a/app/index.js
+++ b/app/index.js
@@ -1,5 +1,6 @@
+import 'react-app-polyfill/ie11';
+import { browserName } from 'react-device-detect';
import 'location-origin';
-
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-intl-redux';
@@ -14,6 +15,7 @@ import { initI18n } from 'i18n';
import configureStore from 'store/configureStore';
import rootReducer from 'state/rootReducer';
import getRoutes from './routes';
+import BrowserWarning from './pages/browser-warning';
const initialStoreState = createStore(rootReducer, {}).getState();
const initialServerState = window.INITIAL_STATE;
@@ -22,10 +24,13 @@ const finalState = Immutable(initialStoreState).merge([initialServerState, initi
deep: true,
});
const store = configureStore(finalState);
+const isIEBrowser = browserName === 'IE';
-render(
-
- {getRoutes()}
- ,
- document.getElementById('root')
-);
+// TODO: Support IE11 in the future.
+render(isIEBrowser ?
+ : (
+
+ {getRoutes()}
+
+ ),
+document.getElementById('root'));
diff --git a/app/pages/browser-warning/BrowserWarning.js b/app/pages/browser-warning/BrowserWarning.js
new file mode 100644
index 000000000..20609b135
--- /dev/null
+++ b/app/pages/browser-warning/BrowserWarning.js
@@ -0,0 +1,43 @@
+import React from 'react';
+
+function BrowserWarning() {
+ return (
+
+
+ Currently, Varaamo does not support Internet Explorer.
+ We are investigating this issue and finding a solution.
+ Meanwhile, use another browser (such as
+ Chrome
+ ,
+ Firefox
+ or
+ Edge
+ ).
+
+
+ Varaamo ei tue Internet Explorer selainta tällä hetkellä.
+ Selvitämme ongelmaa sen ratkaisemiseksi.
+ Sillä välin, käytä toista selainta (kuten
+ Chrome
+ ,
+ Firefox
+ tai
+ Edge
+ ).
+
+
+ Varaamo fungerar inte längre med Internet Explorer.
+ Vi arbetar med att lösa problemet.
+ Under tiden så var vänlig och använd någon annan browser (t.ex
+ Chrome
+ ,
+ Firefox
+ eller
+ Edge
+ ).
+
+
+ );
+}
+
+export default BrowserWarning;
diff --git a/app/pages/browser-warning/BrowserWarning.spec.js b/app/pages/browser-warning/BrowserWarning.spec.js
new file mode 100644
index 000000000..25aabf17e
--- /dev/null
+++ b/app/pages/browser-warning/BrowserWarning.spec.js
@@ -0,0 +1,25 @@
+import React from 'react';
+
+import BrowserWarning from './BrowserWarning';
+import { shallowWithIntl } from 'utils/testUtils';
+
+describe('pages/browser-warning/BrowserWarning', () => {
+ function getWrapper() {
+ return shallowWithIntl();
+ }
+
+ test('renders a browser warning div', () => {
+ const div = getWrapper().find('div');
+ expect(div.length).toBe(1);
+ });
+
+ test('renders a browser warning paragraph', () => {
+ const p = getWrapper().find('p');
+ expect(p.length).toBe(3);
+ });
+
+ test('renders all specified browser links', () => {
+ const a = getWrapper().find('a');
+ expect(a.length).toBe(9);
+ });
+});
diff --git a/app/pages/browser-warning/index.js b/app/pages/browser-warning/index.js
new file mode 100644
index 000000000..2cb4f92e4
--- /dev/null
+++ b/app/pages/browser-warning/index.js
@@ -0,0 +1,3 @@
+import BrowserWarning from './BrowserWarning';
+
+export default BrowserWarning;
diff --git a/app/pages/search/SearchPage.js b/app/pages/search/SearchPage.js
index efc078ebe..659fc1ae4 100644
--- a/app/pages/search/SearchPage.js
+++ b/app/pages/search/SearchPage.js
@@ -2,7 +2,10 @@ import isEqual from 'lodash/isEqual';
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
+import queryString from 'query-string';
import { bindActionCreators } from 'redux';
+import Row from 'react-bootstrap/lib/Row';
+import Col from 'react-bootstrap/lib/Col';
import { searchResources, toggleMap } from 'actions/searchActions';
import { changeSearchFilters } from 'actions/uiActions';
@@ -14,12 +17,14 @@ import ResourceMap from 'shared/resource-map';
import SearchControls from './controls';
import searchPageSelector from './searchPageSelector';
import SearchResults from './results/SearchResults';
+import Sort from './Sort';
import MapToggle from './results/MapToggle';
class UnconnectedSearchPage extends Component {
constructor(props) {
super(props);
this.searchResources = this.searchResources.bind(this);
+ this.sortResource = this.sortResource.bind(this);
}
componentDidMount() {
@@ -68,6 +73,11 @@ class UnconnectedSearchPage extends Component {
this.props.actions.searchResources({ ...filters, ...position });
}
+ sortResource(value) {
+ const filters = { ...this.props.filters, ...{ orderBy: value } };
+ this.props.history.push(`/search?${queryString.stringify(filters)}`);
+ }
+
render() {
const {
actions,
@@ -80,6 +90,7 @@ class UnconnectedSearchPage extends Component {
searchDone,
selectedUnitId,
showMap,
+ filters,
t,
} = this.props;
return (
@@ -96,7 +107,13 @@ class UnconnectedSearchPage extends Component {
showMap={showMap}
/>
)}
+
+
+
+
+
+
{(searchDone || isFetchingSearchResults) && (
{
expect(resourceMap.prop('selectedUnitId')).toBe(props.selectedUnitId);
});
+ test('renders an Row element', () => {
+ expect(getWrapper().find(Row)).toHaveLength(1);
+ });
+
+ test('renders a Sort component with correct props', () => {
+ const sort = getWrapper().find(Sort);
+ expect(sort).toHaveLength(1);
+ expect(typeof sort.prop('onChange')).toBe('function');
+ });
+
describe('SearchResults', () => {
function getSearchResults(props) {
return getWrapper(props).find(SearchResults);
@@ -214,6 +226,23 @@ describe('pages/search/SearchPage', () => {
});
});
+ describe('sortResource', () => {
+ const pushMock = simple.mock();
+ beforeAll(() => {
+ const instance = getWrapper(
+ {
+ history: { push: pushMock }
+ }
+ ).instance();
+ instance.sortResource('name');
+ });
+
+ test('changes history with correct queryString', () => {
+ expect(pushMock.callCount).toBe(1);
+ expect(pushMock.lastCall.args[0]).toContain('name');
+ });
+ });
+
describe('if search filters did change and url has query part', () => {
let nextProps;
diff --git a/app/pages/search/Sort.js b/app/pages/search/Sort.js
new file mode 100644
index 000000000..3c06a5cc1
--- /dev/null
+++ b/app/pages/search/Sort.js
@@ -0,0 +1,50 @@
+import PropTypes from 'prop-types';
+import React, { Component } from 'react';
+import { connect } from 'react-redux';
+
+import { injectT } from 'i18n';
+import CONSTANTS from '../../constants/AppConstants';
+import SelectControl from './controls/SelectControl';
+
+export class UnconnectedSort extends Component {
+ getSortOptions = () => {
+ const { lang, t } = this.props;
+
+ return [
+ { label: t('SortBy.name.label'), value: CONSTANTS.SORT_BY_OPTIONS.NAME.replace('lang', lang) },
+ { label: t('SortBy.type.label'), value: CONSTANTS.SORT_BY_OPTIONS.TYPE.replace('lang', lang) },
+ { label: t('SortBy.premise.label'), value: CONSTANTS.SORT_BY_OPTIONS.PREMISES.replace('lang', lang) },
+ { label: t('SortBy.people.label'), value: CONSTANTS.SORT_BY_OPTIONS.PEOPLE },
+ ];
+ }
+
+ render() {
+ return (
+ this.props.onChange(value)}
+ options={this.getSortOptions()}
+ value={this.props.sortValue}
+ />
+ );
+ }
+}
+
+const mapStateToProps = state => ({
+ lang: state.intl.locale
+});
+
+export default connect(
+ mapStateToProps,
+ {}
+)(injectT(UnconnectedSort));
+
+
+UnconnectedSort.propTypes = {
+ lang: PropTypes.string.isRequired,
+ t: PropTypes.func.isRequired,
+ onChange: PropTypes.func.isRequired,
+ sortValue: PropTypes.string,
+};
diff --git a/app/pages/search/Sort.spec.js b/app/pages/search/Sort.spec.js
new file mode 100644
index 000000000..45b33bad6
--- /dev/null
+++ b/app/pages/search/Sort.spec.js
@@ -0,0 +1,40 @@
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import SelectControl from './controls/SelectControl';
+import { UnconnectedSort as Sort } from './Sort';
+
+describe('pages/search/Sort', () => {
+ const defaultProps = {
+ sortValue: '',
+ t: value => value,
+ lang: 'en',
+ onChange: () => {},
+ };
+
+ function getWrapper(props) {
+ return shallow();
+ }
+
+ describe('pages/search/Sort', () => {
+ test('renders SelectControl for sort with correct props', () => {
+ const wrapper = getWrapper({});
+ const selectControl = wrapper.find(SelectControl);
+ expect(selectControl).toHaveLength(1);
+ expect(selectControl.prop('id')).toBe('app-Sort');
+ expect(selectControl.prop('label')).toEqual('SortBy.label');
+ expect(selectControl.prop('onChange')).toBeDefined();
+ expect(selectControl.prop('options')).toBeDefined();
+ expect(selectControl.prop('value')).toEqual(defaultProps.sortValue);
+ });
+
+ test('get translated options base on language', () => {
+ const wrapper = getWrapper({ lang: 'foo' });
+ const options = wrapper.prop('options');
+
+ expect(options.length).toEqual(4);
+ expect(options[0].value).toContain('foo');
+ expect(options[3].value).not.toContain('foo');
+ });
+ });
+});
diff --git a/app/pages/search/_search-page.scss b/app/pages/search/_search-page.scss
index 45ce5647f..c1c94757b 100644
--- a/app/pages/search/_search-page.scss
+++ b/app/pages/search/_search-page.scss
@@ -13,6 +13,10 @@
position: relative;
}
+ &__sortControlRow {
+ margin-top: 10px;
+ }
+
.app-MapToggle {
background-color: $hel-copper;
display: block;
diff --git a/app/shared/resource-card/ResourceCard.js b/app/shared/resource-card/ResourceCard.js
index e82b39351..1bcbf180b 100644
--- a/app/shared/resource-card/ResourceCard.js
+++ b/app/shared/resource-card/ResourceCard.js
@@ -2,21 +2,30 @@ import classnames from 'classnames';
import round from 'lodash/round';
import PropTypes from 'prop-types';
import queryString from 'query-string';
-import React, { Component } from 'react';
+import React, { Component, Fragment } from 'react';
import { Link } from 'react-router-dom';
-import Col from 'react-bootstrap/lib/Col';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
import iconHome from 'hel-icons/dist/shapes/home.svg';
import iconMapMarker from 'hel-icons/dist/shapes/map-marker.svg';
import iconTicket from 'hel-icons/dist/shapes/ticket.svg';
import iconUser from 'hel-icons/dist/shapes/user-o.svg';
+import iconHeart from 'hel-icons/dist/shapes/heart-o.svg';
-import UnpublishedLabel from 'shared/label/Unpublished';
import { injectT } from 'i18n';
+import iconHeartFilled from 'assets/icons/heart-filled.svg';
+import UnpublishedLabel from 'shared/label/Unpublished';
import iconMap from 'assets/icons/map.svg';
import BackgroundImage from 'shared/background-image';
import { getMainImage } from 'utils/imageUtils';
import { getHourlyPrice, getResourcePageUrlComponents } from 'utils/resourceUtils';
-import ResourceAvailability from './ResourceAvailability';
+import ResourceAvailability from './label';
+import ResourceCardInfoCell from './info';
+import resourceCardSelector from './resourceCardSelector';
+import {
+ favoriteResource,
+ unfavoriteResource
+} from 'actions/resourceActions';
class ResourceCard extends Component {
handleSearchByType = () => {
@@ -59,7 +68,7 @@ class ResourceCard extends Component {
render() {
const {
- date, resource, t, unit
+ date, resource, t, unit, actions, isLoggedIn
} = this.props;
const { pathname, query } = getResourcePageUrlComponents(resource, date);
const linkTo = {
@@ -97,83 +106,76 @@ class ResourceCard extends Component {