From 9ac3f8dfd776b425b90e893d25657d10acadddcf Mon Sep 17 00:00:00 2001 From: Markus Salo Date: Thu, 25 Apr 2019 11:28:20 +0300 Subject: [PATCH 01/56] ResourceCardInfoCell.spec.js: Fix file name Previous one had some invisible unicode symbol in the middle that caused issues --- .../shared/resource-card/info/ResourceCardInfoCell.spec.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename "app/shared/resource-card/info/ResourceCard\bInfoCell.spec.js" => app/shared/resource-card/info/ResourceCardInfoCell.spec.js (100%) diff --git "a/app/shared/resource-card/info/ResourceCard\bInfoCell.spec.js" b/app/shared/resource-card/info/ResourceCardInfoCell.spec.js similarity index 100% rename from "app/shared/resource-card/info/ResourceCard\bInfoCell.spec.js" rename to app/shared/resource-card/info/ResourceCardInfoCell.spec.js From d90c79a2583508b4c88c03ff42831c7fb2e17093 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 29 Apr 2019 10:03:01 +0300 Subject: [PATCH 02/56] fix persisted state of locale is language to locale se --- app/i18n/changeLocale.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/i18n/changeLocale.js b/app/i18n/changeLocale.js index 8f7175578..198d980be 100644 --- a/app/i18n/changeLocale.js +++ b/app/i18n/changeLocale.js @@ -13,8 +13,9 @@ const messages = { }; function changeLocale(language) { - savePersistLocale(language); const locale = language === 'sv' ? 'se' : language; + savePersistLocale(locale); + moment.locale(`varaamo-${locale}`); return updateIntl({ locale, From fede1e63b38540360c519c2fafe95ab3a283ce83 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 29 Apr 2019 13:58:06 +0300 Subject: [PATCH 03/56] add localization for datepicker --- app/shared/date-picker/DatePicker.js | 45 +++++++++++++++++------ app/shared/date-picker/DatePicker.spec.js | 16 +++++++- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/app/shared/date-picker/DatePicker.js b/app/shared/date-picker/DatePicker.js index 8a23f1ece..5a6c6b625 100644 --- a/app/shared/date-picker/DatePicker.js +++ b/app/shared/date-picker/DatePicker.js @@ -1,3 +1,5 @@ +import AppConstants from 'constants/AppConstants'; + import React from 'react'; import PropTypes from 'prop-types'; import DayPickerInput from 'react-day-picker/DayPickerInput'; @@ -5,18 +7,19 @@ import MomentLocaleUtils, { formatDate, parseDate, } from 'react-day-picker/moment'; +import { connect } from 'react-redux'; +import { createStructuredSelector } from 'reselect'; -const dateFormat = 'YYYY-MM-DD'; -const localizedDateFormat = 'D.M.YYYY'; -DatePicker.propTypes = { - dateFormat: PropTypes.string, - onChange: PropTypes.func.isRequired, - value: PropTypes.string.isRequired, -}; +import { currentLanguageSelector } from 'state/selectors/translationSelectors'; + +const defaultDateFormat = 'YYYY-MM-DD'; +const localizedDateFormat = 'D.M.YYYY'; -function DatePicker(props) { - const pickerDateFormat = props.dateFormat || localizedDateFormat; +export function UnconnectedDatePicker({ + dateFormat, onChange, currentLocale, value, rest +}) { + const pickerDateFormat = dateFormat || localizedDateFormat; return ( props.onChange(formatDate(date, dateFormat))} + onDayChange={date => onChange(formatDate(date, defaultDateFormat))} parseDate={parseDate} - value={new Date(props.value)} + value={new Date(value)} + {...rest} /> ); } -export default DatePicker; +UnconnectedDatePicker.propTypes = { + dateFormat: PropTypes.string, + onChange: PropTypes.func.isRequired, + value: PropTypes.string.isRequired, + currentLocale: PropTypes.string, + rest: PropTypes.object +}; + +UnconnectedDatePicker.defaultProps = { + currentLocale: AppConstants.DEFAULT_LOCALE +}; + +const languageSelector = createStructuredSelector({ + currentLocale: currentLanguageSelector +}); + +export default connect(languageSelector)(UnconnectedDatePicker); diff --git a/app/shared/date-picker/DatePicker.spec.js b/app/shared/date-picker/DatePicker.spec.js index ac2ffd252..654610b4a 100644 --- a/app/shared/date-picker/DatePicker.spec.js +++ b/app/shared/date-picker/DatePicker.spec.js @@ -3,7 +3,7 @@ import React from 'react'; import DayPickerInput from 'react-day-picker/DayPickerInput'; import simple from 'simple-mock'; -import DatePicker from './DatePicker'; +import { UnconnectedDatePicker as DatePicker } from './DatePicker'; function getWrapper(props) { const defaults = { @@ -32,5 +32,19 @@ describe('shared/date-picker/DatePicker', () => { expect(onChange.callCount).toBe(1); expect(onChange.lastCall.arg).toBe(expectedDate); }); + + test('have default locale prop', () => { + const defaultLocale = 'fi'; + const dateField = getDateFieldWrapper(); + + expect(dateField.prop('dayPickerProps').locale).toEqual(defaultLocale); + }); + + test('have locale prop passed from redux state', () => { + const mockLocale = 'se'; + const dateField = getDateFieldWrapper({ currentLocale: mockLocale }); + + expect(dateField.prop('dayPickerProps').locale).toEqual(mockLocale); + }); }); }); From 6335858ed9c760e364a75264ce93b667e67cf104 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 29 Apr 2019 15:01:49 +0300 Subject: [PATCH 04/56] use smaller build of moment-timezone to reduce build size, add test for timezone diff --- .travis.yml | 5 +++-- app/i18n/initI18n.js | 3 +-- app/utils/__tests__/timeUtils.spec.js | 7 +++++++ config/jest/setupJest.js | 2 -- package.json | 2 +- yarn.lock | 7 ++++--- 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index a6a3953bf..9d38483d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,12 +18,13 @@ install: # We are running browser tests before_script: + - export TZ=Europe/Helsinki - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start script: - yarn lint - yarn test:ci - + after_success: - - codecov < coverage/lcov.info \ No newline at end of file + - codecov < coverage/lcov.info diff --git a/app/i18n/initI18n.js b/app/i18n/initI18n.js index 3cd25dae7..b95f6178a 100644 --- a/app/i18n/initI18n.js +++ b/app/i18n/initI18n.js @@ -1,7 +1,7 @@ import 'moment/locale/en-gb'; import 'moment/locale/fi'; import 'moment/locale/sv'; -import 'moment-timezone'; +import 'moment-timezone/builds/moment-timezone-with-data-10-year-range'; import constants from 'constants/AppConstants'; @@ -22,7 +22,6 @@ const messages = { fi: fiMessages, se: svMessages, }; -moment.tz.setDefault('Europe/Helsinki'); moment.defineLocale('varaamo-en', { parentLocale: 'en-gb', diff --git a/app/utils/__tests__/timeUtils.spec.js b/app/utils/__tests__/timeUtils.spec.js index 4cb6a0811..d2dd119ed 100644 --- a/app/utils/__tests__/timeUtils.spec.js +++ b/app/utils/__tests__/timeUtils.spec.js @@ -84,6 +84,13 @@ describe('Utils: timeUtils', () => { } ); + test('default timezone is your local timezone', () => { + const timeZoneFromDate = Intl.DateTimeFormat().resolvedOptions().timeZone; + const timeZoneFromMoment = moment.tz.guess(true); + + expect(timeZoneFromMoment).toEqual(timeZoneFromDate); + }); + test( 'returns an object with availableBetween, end and start in correct form when end is 23:30', () => { diff --git a/config/jest/setupJest.js b/config/jest/setupJest.js index 16611a39e..0ebc8eac3 100755 --- a/config/jest/setupJest.js +++ b/config/jest/setupJest.js @@ -9,8 +9,6 @@ require('isomorphic-fetch'); configure({ adapter: new Adapter(), disableLifecycleMethods: true }); -// Adding global locale for all unit test -moment.tz.setDefault('Europe/Helsinki'); moment.locale('fi'); // mock window, jsdom intergrated with Jest diff --git a/package.json b/package.json index fae09e1c0..ab74cc017 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "mobile-detect": "1.4.0", "moment": "2.24.0", "moment-range": "4.0.1", - "moment-timezone": "0.5.23", + "moment-timezone": "0.5.25", "nocache": "2.0.0", "normalizr": "2.2.1", "passport": "0.3.2", diff --git a/yarn.lock b/yarn.lock index 02029819c..a6ae596c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5053,9 +5053,10 @@ moment-range@4.0.1: dependencies: es6-symbol "^3.1.0" -moment-timezone@0.5.23: - version "0.5.23" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.23.tgz#7cbb00db2c14c71b19303cb47b0fb0a6d8651463" +moment-timezone@0.5.25: + version "0.5.25" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.25.tgz#a11bfa2f74e088327f2cd4c08b3e7bdf55957810" + integrity sha512-DgEaTyN/z0HFaVcVbSyVCUU6HeFdnNC3vE4c9cgu2dgMTvjBUBdBzWfasTBmAW45u5OIMeCJtU8yNjM22DHucw== dependencies: moment ">= 2.9.0" From b21502c35958b732db89162e7c43515305e7b26a Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 29 Apr 2019 17:15:43 +0300 Subject: [PATCH 05/56] add test for gen options --- .env.example | 3 +- app/constants/AppConstants.js | 2 +- .../controls/SearchControlsContainer.js | 17 +++++-- .../controls/SearchControlsContainer.spec.js | 49 +++++++++++++++++++ config/webpack.development.js | 1 + config/webpack.production.js | 1 + 6 files changed, 67 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 9887bf701..a2dd26b9e 100644 --- a/.env.example +++ b/.env.example @@ -1,4 +1,5 @@ CLIENT_ID= CLIENT_SECRET= SESSION_SECRET= -TARGET_APP= \ No newline at end of file +TARGET_APP= +CUSTOM_MUNICIPALITY_OPTIONS= diff --git a/app/constants/AppConstants.js b/app/constants/AppConstants.js index d8c11f3ea..97b99f5fb 100644 --- a/app/constants/AppConstants.js +++ b/app/constants/AppConstants.js @@ -45,7 +45,7 @@ export default { }, }, SEARCH_PAGE_SIZE: 30, - SEARCH_MUNICIPALITY_OPTIONS: ['Helsinki', 'Espoo', 'Vantaa'], + DEFAULT_MUNICIPALITY_OPTIONS: ['Helsinki', 'Espoo', 'Vantaa'], SHOW_TEST_SITE_MESSAGE: SETTINGS.SHOW_TEST_SITE_MESSAGE, SUPPORTED_LANGUAGES: ['en', 'fi', 'sv'], SUPPORTED_SEARCH_FILTERS: { diff --git a/app/pages/search/controls/SearchControlsContainer.js b/app/pages/search/controls/SearchControlsContainer.js index 8f590d0a1..d9f758edc 100644 --- a/app/pages/search/controls/SearchControlsContainer.js +++ b/app/pages/search/controls/SearchControlsContainer.js @@ -53,10 +53,19 @@ class UnconnectedSearchControlsContainer extends Component { return options; } - getMunicipalityOptions = () => constants.SEARCH_MUNICIPALITY_OPTIONS.map(municipality => ({ - value: municipality.toLowerCase(), - label: municipality, - })); + getMunicipalityOptions = () => { + let municipalities = constants.DEFAULT_MUNICIPALITY_OPTIONS; + + if (Array.isArray(SETTINGS.CUSTOM_MUNICIPALITY_OPTIONS) + && SETTINGS.CUSTOM_MUNICIPALITY_OPTIONS.length) { + municipalities = SETTINGS.CUSTOM_MUNICIPALITY_OPTIONS; + } + + return municipalities.map(municipality => ({ + value: municipality.toLowerCase(), + label: municipality, + })); + }; handleDateChange = ({ date }) => { const dateInCorrectFormat = moment(date, 'L').format(constants.DATE_FORMAT); diff --git a/app/pages/search/controls/SearchControlsContainer.spec.js b/app/pages/search/controls/SearchControlsContainer.spec.js index 4d039b0a8..7ff723626 100644 --- a/app/pages/search/controls/SearchControlsContainer.spec.js +++ b/app/pages/search/controls/SearchControlsContainer.spec.js @@ -483,4 +483,53 @@ describe('pages/search/controls/SearchControlsContainer', () => { expect(actions.fetchPurposes.callCount).toBe(1); }); }); + + describe('getMunicipalityOptions', () => { + test('return options default from 3 central cities', () => { + const instance = getWrapper().instance(); + const options = instance.getMunicipalityOptions(); + + expect(options[0].label).toEqual(constants.DEFAULT_MUNICIPALITY_OPTIONS[0]); + }); + + test('return options custom from env', () => { + const instance = getWrapper().instance(); + + global.SETTINGS = { + CUSTOM_MUNICIPALITY_OPTIONS: ['Foo', 'Bar'] + }; + + const options = instance.getMunicipalityOptions(); + + expect(options[0].label).toEqual('Foo'); + }); + + test('still work with single item', () => { + const instance = getWrapper().instance(); + + global.SETTINGS = { + CUSTOM_MUNICIPALITY_OPTIONS: ['Foo'] + }; + + const options = instance.getMunicipalityOptions(); + + expect(options[0].label).toEqual('Foo'); + }); + + test('use default in case of error, bad env var', () => { + const instance = getWrapper().instance(); + + global.SETTINGS = { + CUSTOM_MUNICIPALITY_OPTIONS: 'fooo' + }; + + const options = instance.getMunicipalityOptions(); + + expect(options[0].label).toEqual(constants.DEFAULT_MUNICIPALITY_OPTIONS[0]); + }); + + afterAll(() => { + delete global.SETTINGS; + }); + }); }); diff --git a/config/webpack.development.js b/config/webpack.development.js index 6c95c725f..6b1f95cf9 100755 --- a/config/webpack.development.js +++ b/config/webpack.development.js @@ -69,6 +69,7 @@ module.exports = merge(common, { API_URL: JSON.stringify(process.env.API_URL || 'https://api.hel.fi/respa-test/v1'), SHOW_TEST_SITE_MESSAGE: Boolean(process.env.SHOW_TEST_SITE_MESSAGE), TRACKING: Boolean(process.env.PIWIK_SITE_ID), + CUSTOM_MUNICIPALITY_OPTIONS: process.env.CUSTOM_MUNICIPALITY_OPTIONS }, }), new webpack.HotModuleReplacementPlugin(), diff --git a/config/webpack.production.js b/config/webpack.production.js index 0ebf2af80..50acfdbf0 100755 --- a/config/webpack.production.js +++ b/config/webpack.production.js @@ -57,6 +57,7 @@ module.exports = merge(common, { API_URL: JSON.stringify(process.env.API_URL || 'https://api.hel.fi/respa/v1'), SHOW_TEST_SITE_MESSAGE: Boolean(process.env.SHOW_TEST_SITE_MESSAGE), TRACKING: Boolean(process.env.PIWIK_SITE_ID), + CUSTOM_MUNICIPALITY_OPTIONS: process.env.CUSTOM_MUNICIPALITY_OPTIONS, }, }), new MiniCssExtractPlugin({ From bcf81b68f6f1eed2a1502405e7d20a0276e0aaf3 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 29 Apr 2019 17:25:59 +0300 Subject: [PATCH 06/56] add more test and safety check for non-string value --- .../controls/SearchControlsContainer.js | 13 +++++--- .../controls/SearchControlsContainer.spec.js | 31 +++++++++++++++++-- 2 files changed, 37 insertions(+), 7 deletions(-) diff --git a/app/pages/search/controls/SearchControlsContainer.js b/app/pages/search/controls/SearchControlsContainer.js index d9f758edc..46f2aa695 100644 --- a/app/pages/search/controls/SearchControlsContainer.js +++ b/app/pages/search/controls/SearchControlsContainer.js @@ -1,6 +1,7 @@ import constants from 'constants/AppConstants'; import range from 'lodash/range'; +import capitalize from 'lodash/capitalize'; import moment from 'moment'; import PropTypes from 'prop-types'; import queryString from 'query-string'; @@ -61,10 +62,14 @@ class UnconnectedSearchControlsContainer extends Component { municipalities = SETTINGS.CUSTOM_MUNICIPALITY_OPTIONS; } - return municipalities.map(municipality => ({ - value: municipality.toLowerCase(), - label: municipality, - })); + return municipalities.map((municipality) => { + const municipalityStr = typeof municipality === 'string' ? municipality : municipality.toString(); + + return { + value: municipalityStr.toLowerCase(), + label: capitalize(municipalityStr), + }; + }); }; handleDateChange = ({ date }) => { diff --git a/app/pages/search/controls/SearchControlsContainer.spec.js b/app/pages/search/controls/SearchControlsContainer.spec.js index 7ff723626..aa7b21aae 100644 --- a/app/pages/search/controls/SearchControlsContainer.spec.js +++ b/app/pages/search/controls/SearchControlsContainer.spec.js @@ -504,16 +504,16 @@ describe('pages/search/controls/SearchControlsContainer', () => { expect(options[0].label).toEqual('Foo'); }); - test('still work with single item', () => { + test('doesnt work with empty array', () => { const instance = getWrapper().instance(); global.SETTINGS = { - CUSTOM_MUNICIPALITY_OPTIONS: ['Foo'] + CUSTOM_MUNICIPALITY_OPTIONS: [] }; const options = instance.getMunicipalityOptions(); - expect(options[0].label).toEqual('Foo'); + expect(options[0].label).toEqual(constants.DEFAULT_MUNICIPALITY_OPTIONS[0]); }); test('use default in case of error, bad env var', () => { @@ -528,6 +528,31 @@ describe('pages/search/controls/SearchControlsContainer', () => { expect(options[0].label).toEqual(constants.DEFAULT_MUNICIPALITY_OPTIONS[0]); }); + test('still work if value is number instead of string', () => { + const instance = getWrapper().instance(); + + global.SETTINGS = { + CUSTOM_MUNICIPALITY_OPTIONS: [123, 456] + }; + + const options = instance.getMunicipalityOptions(); + + expect(options[0].label).toEqual('123'); + }); + + test('label is capitalized, value is in lowercase', () => { + const instance = getWrapper().instance(); + + global.SETTINGS = { + CUSTOM_MUNICIPALITY_OPTIONS: ['foo'] + }; + + const options = instance.getMunicipalityOptions(); + + expect(options[0].label).toEqual('Foo'); + expect(options[0].value).toEqual('foo'); + }); + afterAll(() => { delete global.SETTINGS; }); From 3869842b04abaa9eb88e0e59086d9c85a58988c3 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Tue, 30 Apr 2019 10:52:38 +0300 Subject: [PATCH 07/56] add guideline for env vars --- .env.example | 2 +- README.md | 25 +++++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/.env.example b/.env.example index a2dd26b9e..740f234ce 100644 --- a/.env.example +++ b/.env.example @@ -2,4 +2,4 @@ CLIENT_ID= CLIENT_SECRET= SESSION_SECRET= TARGET_APP= -CUSTOM_MUNICIPALITY_OPTIONS= +CUSTOM_MUNICIPALITY_OPTIONS=[] diff --git a/README.md b/README.md index bb48a8772..fde6c5225 100644 --- a/README.md +++ b/README.md @@ -62,14 +62,27 @@ By default the running app can be found at `localhost:3000`. ``` OR prepare .env with default content: - + ``` CLIENT_ID CLIENT_SECRET SESSION_SECRET TARGET_APP + API_URL + CUSTOM_MUNICIPALITY_OPTIONS ``` + Environment's variable guideline: + + - `API_URL`: + Custom config to replace global application's api URL. Expected value is valid URL string. + + - `CUSTOM_MUNICIPALITY_OPTIONS`: + Config for custom municipalities. Expected value is array of municipalities: `['example_city']`. + + Without this config, default to use 3 central cities Helsinki, Espoo, Vantaa as options. + + 3. Then, start the development server: ``` @@ -144,7 +157,7 @@ OR enable `eslint --fix` onSave config in your code editor config. ``` $ docker-compose exec web sh ``` -- Remove docker container if needed: +- Remove docker container if needed: ``` $ docker rm -f varaamo-frontend ``` @@ -185,17 +198,17 @@ Running Vscode debugger All setting was included under .vscode directory. - On Chrome: - [Guideline](https://code.visualstudio.com/blogs/2016/02/23/introducing-chrome-debugger-for-vs-code). Setting was under `Vscode debugger` name + [Guideline](https://code.visualstudio.com/blogs/2016/02/23/introducing-chrome-debugger-for-vs-code). Setting was under `Vscode debugger` name - On Jest test: [Guideline](https://jestjs.io/docs/en/troubleshooting#debugging-in-vs-code). Setting was under `Vscode Jest debugger` name. - + - Put breakpoint in test file `(*.spec.js)` - + - Run command: ``` $ yarn test:debug - ``` + ``` License ------- From b21c446ebf6594ee9e8fffc08c04031ea7921988 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Tue, 30 Apr 2019 11:07:28 +0300 Subject: [PATCH 08/56] revise README to be more friendly --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fde6c5225..e8bf86149 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ By default the running app can be found at `localhost:3000`. Custom config to replace global application's api URL. Expected value is valid URL string. - `CUSTOM_MUNICIPALITY_OPTIONS`: - Config for custom municipalities. Expected value is array of municipalities: `['example_city']`. + Config for custom municipalities. Expected value should be array of cities: `['Tampere','Jyväskylä','Oulu']`. Without this config, default to use 3 central cities Helsinki, Espoo, Vantaa as options. From c9460cce55428e9bb69dd2c95c6afd6f069a8eab Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Tue, 30 Apr 2019 13:31:18 +0300 Subject: [PATCH 09/56] change default value of minicipality to array cause of multi select --- app/constants/AppConstants.js | 2 +- app/state/reducers/ui/searchReducer.js | 2 +- app/state/reducers/ui/searchReducer.spec.js | 2 +- app/state/selectors/__tests__/uiSearchFiltersSelector.spec.js | 2 +- app/state/selectors/__tests__/urlSearchFiltersSelector.spec.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/constants/AppConstants.js b/app/constants/AppConstants.js index 97b99f5fb..b92dd8f1c 100644 --- a/app/constants/AppConstants.js +++ b/app/constants/AppConstants.js @@ -53,7 +53,7 @@ export default { date: '', distance: '', duration: 0, - municipality: '', + municipality: [], end: '', lat: '', lon: '', diff --git a/app/state/reducers/ui/searchReducer.js b/app/state/reducers/ui/searchReducer.js index 17ccfbc6a..aa27a0c80 100644 --- a/app/state/reducers/ui/searchReducer.js +++ b/app/state/reducers/ui/searchReducer.js @@ -10,7 +10,7 @@ const initialState = Immutable({ date: '', people: '', purpose: '', - municipality: '', + municipality: [], search: '', distance: '', duration: 0, diff --git a/app/state/reducers/ui/searchReducer.spec.js b/app/state/reducers/ui/searchReducer.spec.js index 63556e6ab..276f816cd 100644 --- a/app/state/reducers/ui/searchReducer.spec.js +++ b/app/state/reducers/ui/searchReducer.spec.js @@ -174,7 +174,7 @@ describe('state/reducers/ui/searchReducer', () => { distance: '', duration: 0, end: '', - municipality: '', + municipality: [], people: '', purpose: '', search: '', diff --git a/app/state/selectors/__tests__/uiSearchFiltersSelector.spec.js b/app/state/selectors/__tests__/uiSearchFiltersSelector.spec.js index 9e445e52d..756ccfd51 100644 --- a/app/state/selectors/__tests__/uiSearchFiltersSelector.spec.js +++ b/app/state/selectors/__tests__/uiSearchFiltersSelector.spec.js @@ -12,7 +12,7 @@ function getState(date = '2015-10-10', start = '08:30', freeOfCharge = '') { distance: '', duration: 30, end: '23:30', - municipality: '', + municipality: [], page: 1, people: '', purpose: 'some-purpose', diff --git a/app/state/selectors/__tests__/urlSearchFiltersSelector.spec.js b/app/state/selectors/__tests__/urlSearchFiltersSelector.spec.js index eaa00a0a8..e1dbfb7f0 100644 --- a/app/state/selectors/__tests__/urlSearchFiltersSelector.spec.js +++ b/app/state/selectors/__tests__/urlSearchFiltersSelector.spec.js @@ -16,7 +16,7 @@ describe('Selector: urlSearchFiltersSelector', () => { start: '08:30', unit: '', useTimeRange: false, - municipality: '', + municipality: [], orderBy: '' }; From 6c9d246e21ce35b0176ef447d0cad5311c64ec0c Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Tue, 30 Apr 2019 13:53:08 +0300 Subject: [PATCH 10/56] add municipality to filter reset list --- app/pages/search/controls/SearchControlsContainer.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/pages/search/controls/SearchControlsContainer.js b/app/pages/search/controls/SearchControlsContainer.js index 46f2aa695..046705dd5 100644 --- a/app/pages/search/controls/SearchControlsContainer.js +++ b/app/pages/search/controls/SearchControlsContainer.js @@ -2,6 +2,7 @@ import constants from 'constants/AppConstants'; import range from 'lodash/range'; import capitalize from 'lodash/capitalize'; +import isEmpty from 'lodash/isEmpty'; import moment from 'moment'; import PropTypes from 'prop-types'; import queryString from 'query-string'; @@ -130,8 +131,8 @@ class UnconnectedSearchControlsContainer extends Component { hasAdvancedFilters() { const { filters, position } = this.props; let hasFilters = Boolean(position); - ['freeOfCharge', 'end', 'distance', 'duration', 'purpose', 'start', 'unit'].forEach((key) => { - if (filters[key]) { + ['freeOfCharge', 'end', 'distance', 'duration', 'purpose', 'start', 'unit', 'municipality'].forEach((key) => { + if (!isEmpty(filters[key])) { hasFilters = true; } }); From 688c4945ba6054504fad5c12fb3396f321affeaf Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Tue, 30 Apr 2019 14:00:23 +0300 Subject: [PATCH 11/56] whitelist municipality out of filter --- app/shared/resource-map/mapSelector.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/shared/resource-map/mapSelector.js b/app/shared/resource-map/mapSelector.js index 6c79f0f44..2cbffaec3 100644 --- a/app/shared/resource-map/mapSelector.js +++ b/app/shared/resource-map/mapSelector.js @@ -19,7 +19,7 @@ const shouldMapFitBoundariesSelector = createSelector( urlSearchFiltersSelector, selectedUnitIdSelector, (filters, unitId) => ( - Boolean(find(omit(filters, ['date', 'duration', 'end', 'page', 'start']), filter => filter !== '' && filter !== false)) || Boolean(unitId) + Boolean(find(omit(filters, ['date', 'duration', 'end', 'page', 'start', 'municipality']), filter => filter !== '' && filter !== false)) || Boolean(unitId) ) ); From 3d48b657e1c269e3babd030a48fdc4ea2add3884 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Fri, 3 May 2019 15:38:31 +0300 Subject: [PATCH 12/56] use slot constant, add unit test for getSlots --- app/constants/SlotConstants.js | 2 ++ .../reservation-calendar/reservationCalendarSelector.js | 3 ++- app/utils/__tests__/timeUtils.spec.js | 8 ++++++++ app/utils/timeUtils.js | 8 +++++++- 4 files changed, 19 insertions(+), 2 deletions(-) diff --git a/app/constants/SlotConstants.js b/app/constants/SlotConstants.js index 2dbc1b00d..44ea18d62 100644 --- a/app/constants/SlotConstants.js +++ b/app/constants/SlotConstants.js @@ -1,3 +1,5 @@ export const slotSize = 30; // minutes export const slotWidth = 30; // pixels export const slotMargin = 0; // pixels + +export const DEFAULT_SLOT_SIZE = '00:30:00'; diff --git a/app/pages/resource/reservation-calendar/reservationCalendarSelector.js b/app/pages/resource/reservation-calendar/reservationCalendarSelector.js index d48839437..6b3951806 100644 --- a/app/pages/resource/reservation-calendar/reservationCalendarSelector.js +++ b/app/pages/resource/reservation-calendar/reservationCalendarSelector.js @@ -1,4 +1,5 @@ import ActionTypes from 'constants/ActionTypes'; +import { DEFAULT_SLOT_SIZE } from 'constants/SlotConstants'; import filter from 'lodash/filter'; import isEmpty from 'lodash/isEmpty'; @@ -76,7 +77,7 @@ const timeSlotsSelector = createSelector( toEditSelector, (resourceDates, reservationsToEdit) => resourceDates.map((resource) => { const { closes, opens } = getOpeningHours(resource); - const period = resource.minPeriod ? resource.minPeriod : undefined; + const period = resource.slot_size || DEFAULT_SLOT_SIZE; const reservations = getOpenReservations(resource); const timeSlots = getTimeSlots(opens, closes, period, reservations, reservationsToEdit); if (timeSlots.length) { diff --git a/app/utils/__tests__/timeUtils.spec.js b/app/utils/__tests__/timeUtils.spec.js index d2dd119ed..dcd56a02d 100644 --- a/app/utils/__tests__/timeUtils.spec.js +++ b/app/utils/__tests__/timeUtils.spec.js @@ -281,6 +281,14 @@ describe('Utils: timeUtils', () => { const end = '2015-10-09T10:00:00+03:00'; const period = '00:30:00'; + test('default period to 30 mins', () => { + const slots = getTimeSlots(start, end); + const startTime = moment(slots[0].start); + const endTime = moment(slots[0].end); + + expect(moment.duration(endTime.diff(startTime)).asMinutes()).toEqual(30); + }); + test('returns an empty array if start is missing', () => { const actual = getTimeSlots(undefined, end, period); diff --git a/app/utils/timeUtils.js b/app/utils/timeUtils.js index 55ffb16bd..7dea9ea13 100644 --- a/app/utils/timeUtils.js +++ b/app/utils/timeUtils.js @@ -1,4 +1,5 @@ import constants from 'constants/AppConstants'; +import { DEFAULT_SLOT_SIZE } from 'constants/SlotConstants'; import forEach from 'lodash/forEach'; import map from 'lodash/map'; @@ -89,7 +90,12 @@ function getStartTimeString(startTime) { return startTime; } -function getTimeSlots(start, end, period = '00:30:00', reservations = [], reservationsToEdit = []) { +function getTimeSlots( + start, end, + period = DEFAULT_SLOT_SIZE, + reservations = [], + reservationsToEdit = [] +) { if (!start || !end) { return []; } From bd0da005df8aa63f27ef7b0c86dc9702b493732d Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Fri, 3 May 2019 15:42:06 +0300 Subject: [PATCH 13/56] fix unit test with new field when creating slots --- .../reservationCalendarSelector.spec.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js b/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js index 3a40e5968..ab3b9f07b 100644 --- a/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js +++ b/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js @@ -48,7 +48,7 @@ function getProps(id = 'some-id', date = '2015-10-10') { describe('pages/resource/reservation-calendar/reservationCalendarSelector', () => { const resource = Resource.build({ availableHours, - minPeriod: '01:00:00', + slot_size: '01:00:00', openingHours: openingHoursMonth, reservations: [ { @@ -163,7 +163,7 @@ describe('pages/resource/reservation-calendar/reservationCalendarSelector', () = const actualArgs = timeUtils.getTimeSlots.calls[5].args; expect(actualArgs[0]).toBe('2015-10-10T12:00:00+03:00'); expect(actualArgs[1]).toBe('2015-10-10T18:00:00+03:00'); - expect(actualArgs[2]).toBe(resource.minPeriod); + expect(actualArgs[2]).toBe(resource.slot_size); expect(actualArgs[3]).toEqual(resource.reservations); expect(selected.timeSlots).toEqual(expectedMockSlots); simple.restore(); @@ -190,7 +190,7 @@ describe('pages/resource/reservation-calendar/reservationCalendarSelector', () = const actualArgs = timeUtils.getTimeSlots.calls[5].args; expect(actualArgs[0]).toBe('2015-10-10T12:00:00+03:00'); expect(actualArgs[1]).toBe('2015-10-10T18:00:00+03:00'); - expect(actualArgs[2]).toBe(resource.minPeriod); + expect(actualArgs[2]).toBe(resource.slot_size); expect(actualArgs[3]).toEqual(resource.reservations); expect(selected.timeSlots).toEqual(expectedMockSlots); simple.restore(); From 401e1a89cff962314a954d43d84248c17676c384 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Fri, 3 May 2019 15:49:35 +0300 Subject: [PATCH 14/56] add default slot size to test fixture, fix unit test, replace minPeriod with slotSize --- .../reservation-controls/ReservationControlsContainer.js | 2 +- .../ReservationControlsContainer.spec.js | 4 ++-- app/state/reducers/ui/reservationsReducer.js | 4 ++-- app/state/reducers/ui/reservationsReducer.spec.js | 6 +++--- app/utils/fixtures/Resource.js | 5 ++++- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/app/shared/reservation-controls/ReservationControlsContainer.js b/app/shared/reservation-controls/ReservationControlsContainer.js index fd8a65e85..3e814f269 100644 --- a/app/shared/reservation-controls/ReservationControlsContainer.js +++ b/app/shared/reservation-controls/ReservationControlsContainer.js @@ -58,7 +58,7 @@ export class UnconnectedReservationControlsContainer extends Component { } = this.props; const nextUrl = getEditReservationUrl(reservation); - actions.selectReservationToEdit({ reservation, minPeriod: resource.minPeriod }); + actions.selectReservationToEdit({ reservation, slotSize: resource.slot_size }); history.push(nextUrl); } diff --git a/app/shared/reservation-controls/ReservationControlsContainer.spec.js b/app/shared/reservation-controls/ReservationControlsContainer.spec.js index 699a0ab72..3d8cc4d42 100644 --- a/app/shared/reservation-controls/ReservationControlsContainer.spec.js +++ b/app/shared/reservation-controls/ReservationControlsContainer.spec.js @@ -90,12 +90,12 @@ describe('shared/reservation-controls/ReservationControlsContainer', () => { }); test( - 'calls props.actions.selectReservationToEdit with reservation and minPeriod', + 'calls props.actions.selectReservationToEdit with reservation and slotSize', () => { expect(props.actions.selectReservationToEdit.callCount).toBe(1); expect(props.actions.selectReservationToEdit.lastCall.args[0]).toEqual({ reservation: props.reservation, - minPeriod: props.resource.minPeriod, + slotSize: props.resource.slot_size, }); } ); diff --git a/app/state/reducers/ui/reservationsReducer.js b/app/state/reducers/ui/reservationsReducer.js index a2173a2c8..9df2def71 100644 --- a/app/state/reducers/ui/reservationsReducer.js +++ b/app/state/reducers/ui/reservationsReducer.js @@ -26,8 +26,8 @@ const initialState = Immutable({ }); function selectReservationToEdit(state, action) { - const { minPeriod, reservation } = action.payload; - const slots = getTimeSlots(reservation.begin, reservation.end, minPeriod); + const { slotSize, reservation } = action.payload; + const slots = getTimeSlots(reservation.begin, reservation.end, slotSize); const firstSlot = first(slots); const selected = [ { diff --git a/app/state/reducers/ui/reservationsReducer.spec.js b/app/state/reducers/ui/reservationsReducer.spec.js index d57dac707..e78f2418e 100644 --- a/app/state/reducers/ui/reservationsReducer.spec.js +++ b/app/state/reducers/ui/reservationsReducer.spec.js @@ -385,15 +385,15 @@ describe('state/reducers/ui/reservationsReducer', () => { test('splits the given reservation to slots and add to selected', () => { const begin = '2015-10-09T08:00:00+03:00'; const end = '2015-10-09T10:00:00+03:00'; - const minPeriod = '00:30:00'; + const slotSize = '00:30:00'; const reservation = Reservation.build({ begin, end }); const initialState = Immutable({ selected: [], toEdit: [], }); - const action = selectReservationToEdit({ reservation, minPeriod }); + const action = selectReservationToEdit({ reservation, slotSize }); const nextState = reservationsReducer(initialState, action); - const slots = getTimeSlots(reservation.begin, reservation.end, minPeriod); + const slots = getTimeSlots(reservation.begin, reservation.end, slotSize); const firstSlot = first(slots); const lastSlot = last(slots); const expected = [ diff --git a/app/utils/fixtures/Resource.js b/app/utils/fixtures/Resource.js index dc5d00046..9d958d54a 100644 --- a/app/utils/fixtures/Resource.js +++ b/app/utils/fixtures/Resource.js @@ -1,3 +1,5 @@ +import { DEFAULT_SLOT_SIZE } from 'constants/SlotConstants'; + import { Factory } from 'rosie'; const Resource = new Factory() @@ -14,5 +16,6 @@ const Resource = new Factory() .attr('reservableAfter', null) .attr('supportedReservationExtraFields', []) .attr('userPermissions', { isAdmin: false, canMakeReservations: true }) - .attr('isFavorite', false); + .attr('isFavorite', false) + .attr('slot_size', DEFAULT_SLOT_SIZE); export default Resource; From c3c80ca711ded33aec801a27f32d1785af78f455 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Fri, 3 May 2019 15:54:52 +0300 Subject: [PATCH 15/56] replace all minPeriod with slot_size, all 30min with proper constant --- app/pages/resource/reservation-calendar/utils.spec.js | 6 +++--- app/shared/form-fields/ReservationTimeControls.js | 3 ++- .../modals/reservation-info/ReservationEditForm.js | 2 +- .../resourceCalendarSelector.spec.js | 2 +- app/state/reducers/ui/reservationsReducer.spec.js | 4 +++- app/utils/__tests__/timeUtils.spec.js | 11 ++++++----- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/pages/resource/reservation-calendar/utils.spec.js b/app/pages/resource/reservation-calendar/utils.spec.js index fa88a0f0d..e7de0188d 100644 --- a/app/pages/resource/reservation-calendar/utils.spec.js +++ b/app/pages/resource/reservation-calendar/utils.spec.js @@ -1,5 +1,5 @@ import { openingHoursMonth } from 'constants/ResourceConstants'; - +import { DEFAULT_SLOT_SIZE } from 'constants/SlotConstants'; // import moment from 'moment'; import Resource from 'utils/fixtures/Resource'; @@ -132,7 +132,7 @@ describe('pages/resource/reservation-calendar/utils', () => { start: '2015-10-10T13:00:00Z', end: '2015-10-10T13:30:00Z', }; - const resourceMaxPeriod = Resource.build({ maxPeriod: '00:30:00' }); + const resourceMaxPeriod = Resource.build({ maxPeriod: DEFAULT_SLOT_SIZE }); const actual = utils.isSlotSelectable(slotAfterMaxPeriod, selected, resourceMaxPeriod, lastSelectableFound, isAdmin); expect(actual).toBe(false); @@ -146,7 +146,7 @@ describe('pages/resource/reservation-calendar/utils', () => { start: '2015-10-10T13:00:00Z', end: '2015-10-10T13:30:00Z', }; - const resourceMaxPeriod = Resource.build({ maxPeriod: '00:30:00' }); + const resourceMaxPeriod = Resource.build({ maxPeriod: DEFAULT_SLOT_SIZE }); const actual = utils.isSlotSelectable(slotAfterMaxPeriod, selected, resourceMaxPeriod, lastSelectableFound, true); expect(actual).toBe(true); diff --git a/app/shared/form-fields/ReservationTimeControls.js b/app/shared/form-fields/ReservationTimeControls.js index 49b7dae3c..b682ebf35 100644 --- a/app/shared/form-fields/ReservationTimeControls.js +++ b/app/shared/form-fields/ReservationTimeControls.js @@ -1,4 +1,5 @@ import constants from 'constants/AppConstants'; +import { DEFAULT_SLOT_SIZE } from 'constants/SlotConstants'; import Moment from 'moment'; import { extendMoment } from 'moment-range'; @@ -41,7 +42,7 @@ class ReservationTimeControls extends Component { }; static defaultProps = { - period: '00:30:00', + period: DEFAULT_SLOT_SIZE, timeFormat: 'HH:mm', }; diff --git a/app/shared/modals/reservation-info/ReservationEditForm.js b/app/shared/modals/reservation-info/ReservationEditForm.js index 5acbd76a4..f103461fa 100644 --- a/app/shared/modals/reservation-info/ReservationEditForm.js +++ b/app/shared/modals/reservation-info/ReservationEditForm.js @@ -109,7 +109,7 @@ class UnconnectedReservationEditForm extends Component { diff --git a/app/shared/resource-calendar/resourceCalendarSelector.spec.js b/app/shared/resource-calendar/resourceCalendarSelector.spec.js index 30277f480..ee70533a3 100644 --- a/app/shared/resource-calendar/resourceCalendarSelector.spec.js +++ b/app/shared/resource-calendar/resourceCalendarSelector.spec.js @@ -43,7 +43,7 @@ describe('shared/resource-calendar/resourceCalendarSelector', () => { ends: '2015-10-11T20:00:00+03:00', }, ], - minPeriod: '01:00:00', + slot_size: '01:00:00', openingHours: openingHoursMonth, reservations: [ // Day 2015-10-10 is partially available diff --git a/app/state/reducers/ui/reservationsReducer.spec.js b/app/state/reducers/ui/reservationsReducer.spec.js index e78f2418e..0ebe158e3 100644 --- a/app/state/reducers/ui/reservationsReducer.spec.js +++ b/app/state/reducers/ui/reservationsReducer.spec.js @@ -1,10 +1,12 @@ import types from 'constants/ActionTypes'; +import { DEFAULT_SLOT_SIZE } from 'constants/SlotConstants'; import { createAction } from 'redux-actions'; import Immutable from 'seamless-immutable'; import first from 'lodash/first'; import last from 'lodash/last'; + import { cancelReservationEdit, changeAdminReservationFilters, @@ -385,7 +387,7 @@ describe('state/reducers/ui/reservationsReducer', () => { test('splits the given reservation to slots and add to selected', () => { const begin = '2015-10-09T08:00:00+03:00'; const end = '2015-10-09T10:00:00+03:00'; - const slotSize = '00:30:00'; + const slotSize = DEFAULT_SLOT_SIZE; const reservation = Reservation.build({ begin, end }); const initialState = Immutable({ selected: [], diff --git a/app/utils/__tests__/timeUtils.spec.js b/app/utils/__tests__/timeUtils.spec.js index dcd56a02d..4d0805621 100644 --- a/app/utils/__tests__/timeUtils.spec.js +++ b/app/utils/__tests__/timeUtils.spec.js @@ -1,4 +1,5 @@ import constants from 'constants/AppConstants'; +import { DEFAULT_SLOT_SIZE } from 'constants/SlotConstants'; import MockDate from 'mockdate'; import Moment from 'moment'; @@ -279,7 +280,7 @@ describe('Utils: timeUtils', () => { describe('When critical info is missing', () => { const start = '2015-10-09T08:00:00+03:00'; const end = '2015-10-09T10:00:00+03:00'; - const period = '00:30:00'; + const period = DEFAULT_SLOT_SIZE; test('default period to 30 mins', () => { const slots = getTimeSlots(start, end); @@ -322,7 +323,7 @@ describe('Utils: timeUtils', () => { describe('When dividing 2 hours into 30 min slots', () => { const start = '2015-10-09T08:00:00+03:00'; const end = '2015-10-09T10:00:00+03:00'; - const period = '00:30:00'; + const period = DEFAULT_SLOT_SIZE; const duration = moment.duration(period); const slots = getTimeSlots(start, end, period); @@ -406,7 +407,7 @@ describe('Utils: timeUtils', () => { describe('slot reserved property', () => { const start = '2015-10-09T08:00:00+03:00'; const end = '2015-10-09T10:00:00+03:00'; - const period = '00:30:00'; + const period = DEFAULT_SLOT_SIZE; describe('with one reservation', () => { const reservations = [ @@ -462,7 +463,7 @@ describe('Utils: timeUtils', () => { describe('slot reservationStarting and reservationEnding properties during reservation', () => { const start = '2015-10-09T08:00:00+03:00'; const end = '2015-10-09T09:30:00+03:00'; - const period = '00:30:00'; + const period = DEFAULT_SLOT_SIZE; const reservations = [ { begin: '2015-10-09T08:00:00+03:00', @@ -487,7 +488,7 @@ describe('Utils: timeUtils', () => { describe('slot editing property', () => { const start = '2015-10-09T08:00:00+03:00'; const end = '2015-10-09T10:00:00+03:00'; - const period = '00:30:00'; + const period = DEFAULT_SLOT_SIZE; const reservations = []; describe('with one reservation to edit', () => { From cdf966fda75323055a024ad4995f21421b31d780 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Fri, 3 May 2019 16:03:58 +0300 Subject: [PATCH 16/56] remove invalid test --- app/utils/__tests__/timeUtils.spec.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/app/utils/__tests__/timeUtils.spec.js b/app/utils/__tests__/timeUtils.spec.js index 4d0805621..9f59b2b87 100644 --- a/app/utils/__tests__/timeUtils.spec.js +++ b/app/utils/__tests__/timeUtils.spec.js @@ -302,12 +302,6 @@ describe('Utils: timeUtils', () => { expect(actual).toEqual([]); }); - test('uses 30 minutes as default duration if period is missing', () => { - const actual = getTimeSlots(start, end, undefined); - - expect(actual.length).toBe(4); - }); - test( 'uses empty array as default reservations if no reservations is given', () => { From b47a8f15a9090791146efe0073b514378b1254e8 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 6 May 2019 14:37:24 +0300 Subject: [PATCH 17/56] add timeslot minPeriod check and unit tests --- app/i18n/messages/en.json | 1 + .../time-slots/TimeSlot.js | 24 +++++++++++++- .../time-slots/TimeSlots.js | 14 ++++++-- .../time-slots/TimeSlots.spec.js | 2 ++ .../resource/reservation-calendar/utils.js | 22 +++++++++++++ .../reservation-calendar/utils.spec.js | 32 +++++++++++++++++++ 6 files changed, 91 insertions(+), 4 deletions(-) diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index 9f505c9a3..3ead24636 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -123,6 +123,7 @@ "Notifications.loginToReserve": "Log in to reserve these premises.", "Notifications.reservationDeleteSuccessMessage": "Your reservation was successfully cancelled.", "Notifications.reservationUpdateSuccessMessage": "Your reservation has been updated.", + "Notifications.selectTimeToReserve.warning": "Mimimum time reservation is required, please select earlier time slot", "Notifications.selectTimeToReserve": "Select the time for which you wish to make the reservation", "Partners.kaupunginkirjatoImageAlt": "Helsinki City Library", "Partners.nuorisoasiainkeskusImageAlt": "City of Helsinki – Youth Department", diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js index 4d1ef1972..27c46d06a 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js @@ -16,6 +16,7 @@ class TimeSlot extends PureComponent { isHighlighted: PropTypes.bool.isRequired, isLoggedIn: PropTypes.bool.isRequired, isSelectable: PropTypes.bool.isRequired, + isUnderMinPeriod: PropTypes.bool.isRequired, onClear: PropTypes.func.isRequired, onClick: PropTypes.func.isRequired, onMouseEnter: PropTypes.func.isRequired, @@ -61,9 +62,28 @@ class TimeSlot extends PureComponent { }; } + /** + * Render notification warning message when user + * trying to select a timeslot that are able to shorter than reservation minPeriod. + * + * For example: last reservation close at 3pm, minPeriod = 3h, warn user when + * select time slot later on 12am + * + * @memberof TimeSlot + */ + renderMinPeriodWarning = () => { + const { t, addNotification } = this.props; + + addNotification({ + message: t('Notifications.selectTimeToReserve.warning'), + type: 'info', + timeOut: 10000, + }); + } + handleClick = (disabled) => { const { - addNotification, isLoggedIn, onClick, resource, slot, t + addNotification, isLoggedIn, onClick, resource, slot, t, isUnderMinPeriod } = this.props; if (disabled) { @@ -71,6 +91,8 @@ class TimeSlot extends PureComponent { if (notification && notification.message) { addNotification(notification); } + } if (isUnderMinPeriod) { + this.renderMinPeriodWarning(); } else { onClick({ begin: slot.start, diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js index 7712d7738..fe44471a4 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js @@ -134,7 +134,9 @@ class TimeSlots extends Component { } renderTimeSlots = () => { - const { selected, selectedDate, slots } = this.props; + const { + selected, selectedDate, slots, resource + } = this.props; let lastSelectableFound = false; const { mobilePlaceholderOffset, timeSlotPlaceholderSizes } = this.calculatePlaceholders( @@ -147,6 +149,7 @@ class TimeSlots extends Component { return null; } const slot = timeSlots[0]; + const lastSlot = timeSlots[timeSlots.length - 1]; const placeholderSize = timeSlotPlaceholderSizes[index]; const slotDate = moment(slot.start).format(constants.DATE_FORMAT); @@ -171,17 +174,21 @@ class TimeSlots extends Component { )} {timeSlots.map((timeSlot) => { + const isUnderMinPeriod = utils.isSlotOverMinPeriod( + selected, timeSlot, lastSlot, resource.minPeriod + ); + if (!lastSelectableFound && selected.length && timeSlot.reserved) { lastSelectableFound = utils.isSlotAfterSelected(timeSlot, selected); } - return this.renderTimeSlot(timeSlot, lastSelectableFound); + return this.renderTimeSlot(timeSlot, lastSelectableFound, isUnderMinPeriod); })} ); }); }; - renderTimeSlot = (slot, lastSelectableFound) => { + renderTimeSlot = (slot, lastSelectableFound, isUnderMinPeriod) => { const { addNotification, isAdmin, @@ -233,6 +240,7 @@ class TimeSlots extends Component { isHighlighted={isHighlighted} isLoggedIn={isLoggedIn} isSelectable={isSelectable} + isUnderMinPeriod={isUnderMinPeriod} key={slot.start} onClear={this.onClear} onClick={onClick} diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.spec.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.spec.js index 15965c19a..37620d8e0 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.spec.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.spec.js @@ -39,6 +39,7 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlots', () => { isEditing: false, isFetching: false, isLoggedIn: true, + isUnderMinPeriod: false, onClear: simple.stub(), onClick: simple.stub(), resource: Resource.build(), @@ -78,6 +79,7 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlots', () => { expect(timeSlot.props().isAdmin).toBe(defaultProps.isAdmin); expect(timeSlot.props().isEditing).toBe(defaultProps.isEditing); expect(timeSlot.props().isLoggedIn).toBe(defaultProps.isLoggedIn); + expect(timeSlot.props().isUnderMinPeriod).toBe(defaultProps.isUnderMinPeriod); expect(timeSlot.props().isStaff).toBe(defaultProps.isStaff); expect(timeSlot.props().onClick).toBe(defaultProps.onClick); expect(timeSlot.props().resource).toEqual(defaultProps.resource); diff --git a/app/pages/resource/reservation-calendar/utils.js b/app/pages/resource/reservation-calendar/utils.js index df7f97a91..a84f01109 100644 --- a/app/pages/resource/reservation-calendar/utils.js +++ b/app/pages/resource/reservation-calendar/utils.js @@ -115,6 +115,27 @@ function isHighlighted(slot, selected, hovered) { && firstSelectedDate.getDate() === slotStartDate.getDate() ); } +/** + * Check if current slot will over minPeriod + * by adding minPeriod to slot start time + * and compare with last slot end time + * + * @param {Object} selected + * @param {Object} slot + * @param {Object} lastSlot + * @param {String | undefined} minPeriod: minPeriod limit, usuall HH:MM:SS + * @returns + */ +function isUnderMinPeriod(selected, slot, lastSlot, minPeriod) { + let isUnder = false; + + if (!selected.length && minPeriod) { + const minPeriodInMinutes = moment.duration(minPeriod).asMinutes(); + isUnder = moment(slot.start).add(minPeriodInMinutes, 'minutes') > (moment(lastSlot.end)); + } + + return isUnder; +} export default { getNextDayFromDate, @@ -126,4 +147,5 @@ export default { isSlotSelectable, isSlotSelected, isFirstSelected, + isUnderMinPeriod, }; diff --git a/app/pages/resource/reservation-calendar/utils.spec.js b/app/pages/resource/reservation-calendar/utils.spec.js index e7de0188d..346e4a55e 100644 --- a/app/pages/resource/reservation-calendar/utils.spec.js +++ b/app/pages/resource/reservation-calendar/utils.spec.js @@ -192,4 +192,36 @@ describe('pages/resource/reservation-calendar/utils', () => { expect(actual).toBe(false); }); }); + + describe('isUnderMinPeriod', () => { + const minPeriod = '1:00:00'; + const lastSlot = { + start: '2015-10-10T15:00:00Z', + end: '2015-10-10T15:30:00Z', + }; + + const inValidSlot = { + start: '2015-10-10T14:30:00Z', + end: '2015-10-10T15:00:00Z', + }; + test('returns false if minPeriod is not defined', () => { + const actual = utils.isUnderMinPeriod(selected); + expect(actual).toBe(false); + }); + + test('returns false if minPeriod is defined but start time slot already selected (allow time slot to be selected if start time is already selected)', () => { + const actual = utils.isUnderMinPeriod(selected, null, null, minPeriod); + expect(actual).toBe(false); + }); + + test('return false if its safe to select slot', () => { + const actual = utils.isUnderMinPeriod(selected, slot, lastSlot, minPeriod); + expect(actual).toBe(false); + }); + + test('return true if selected slot will not fulfill minPeriod', () => { + const actual = utils.isUnderMinPeriod(selected, inValidSlot, lastSlot, minPeriod); + expect(actual).toBe(false); + }); + }); }); From e154e9ea417eeec809294ce8df4ba28918e16893 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 6 May 2019 15:01:33 +0300 Subject: [PATCH 18/56] add unit test for warning toggle --- .../time-slots/TimeSlot.js | 2 +- .../time-slots/TimeSlot.spec.js | 34 +++++++++++++++++++ .../time-slots/TimeSlots.js | 2 +- 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js index 27c46d06a..c9583a9de 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js @@ -91,7 +91,7 @@ class TimeSlot extends PureComponent { if (notification && notification.message) { addNotification(notification); } - } if (isUnderMinPeriod) { + } else if (isUnderMinPeriod) { this.renderMinPeriodWarning(); } else { onClick({ diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js index ae63575ed..728cca61f 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js @@ -14,6 +14,7 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlot', () => { isAdmin: false, isEditing: true, isHighlighted: false, + isUnderMinPeriod: false, isLoggedIn: true, isSelectable: true, onClear: simple.stub(), @@ -197,6 +198,39 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlot', () => { }); }); + describe('show warning notification', () => { + const addNotification = simple.stub(); + const onClick = simple.stub(); + const t = jest.fn(); + let instance; + let wrapper; + + beforeAll(() => { + wrapper = getWrapper({ + addNotification, + isLoggedIn: true, + onClick, + isUnderMinPeriod: true + }); + instance = wrapper.instance(); + instance.handleClick(false); + }); + + afterAll(() => { + simple.restore(); + }); + + test('when isUnderMinPeriod is true', () => { + expect(onClick.callCount).toBe(0); + expect(addNotification.callCount).toBe(1); + expect(addNotification.lastCall.args).toEqual([{ + message: 'Notifications.selectTimeToReserve.warning', + type: 'info', + timeOut: 10000, + }]); + }); + }); + test('when disabled is false', () => { const addNotification = simple.stub(); const onClick = simple.stub(); diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js index fe44471a4..5d5f9eb69 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js @@ -174,7 +174,7 @@ class TimeSlots extends Component { )} {timeSlots.map((timeSlot) => { - const isUnderMinPeriod = utils.isSlotOverMinPeriod( + const isUnderMinPeriod = utils.isUnderMinPeriod( selected, timeSlot, lastSlot, resource.minPeriod ); From dcea61c1c79e680aa0a9837c848a2fc27020373b Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 6 May 2019 15:13:16 +0300 Subject: [PATCH 19/56] add basic translation, fix typo in test --- app/i18n/messages/fi.json | 1 + app/i18n/messages/sv.json | 1 + .../resource/reservation-calendar/time-slots/TimeSlot.spec.js | 1 - 3 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index 34a6981eb..ed1e03032 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -123,6 +123,7 @@ "Notifications.loginToReserve": "Kirjaudu sisään tehdäksesi varauksen tähän tilaan.", "Notifications.reservationDeleteSuccessMessage": "Varauksen peruminen onnistui.", "Notifications.reservationUpdateSuccessMessage": "Varaus päivitetty.", + "Notifications.selectTimeToReserve.warning": "The reservation overlaps with the existing reservation or is outside the opening hours.", "Notifications.selectTimeToReserve": "Valitse aika, jolle haluat tehdä varauksen.", "Partners.kaupunginkirjatoImageAlt": "Helsingin kaupunginkirjasto", "Partners.nuorisoasiainkeskusImageAlt": "Helsingin kaupunki - nuorisoasiainkeskus", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index 5e018021c..b03c0b00d 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -125,6 +125,7 @@ "Notifications.loginToReserve": "Logga in för att boka detta utrymme.", "Notifications.reservationDeleteSuccessMessage": "Bokningen avbokades.", "Notifications.reservationUpdateSuccessMessage": "Bokningen uppdaterades.", + "Notifications.selectTimeToReserve.warning": "The reservation overlaps with the existing reservation or is outside the opening hours.", "Notifications.selectTimeToReserve": "Välj en tidpunkt som du vill boka.", "Partners.kaupunginkirjatoImageAlt": "Helsingfors stadsbibliotek", "Partners.nuorisoasiainkeskusImageAlt": "Helsingfors stad - ungdomscentralen", diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js index 728cca61f..48b1e7555 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js @@ -201,7 +201,6 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlot', () => { describe('show warning notification', () => { const addNotification = simple.stub(); const onClick = simple.stub(); - const t = jest.fn(); let instance; let wrapper; From 143271434f7980d3ad25e8de90ac3e9d870a9f73 Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 8 May 2019 10:41:32 +0300 Subject: [PATCH 20/56] Change reservation confirmation texts --- app/i18n/messages/en.json | 2 +- app/i18n/messages/fi.json | 2 +- app/i18n/messages/sv.json | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index 9f505c9a3..06043f82c 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -160,7 +160,7 @@ "ReservationConfirmation.reservationEditedTitle": "Reservation updated", "ReservationConfirmation.resourceButton": "Back to premise", "ReservationConfirmation.ownReservationButton": "Back to your reservations", - "ReservationConfirmation.confirmationText": "You will receive email ({email}) confirmation for your reservation.", + "ReservationConfirmation.confirmationText": "You will receive email ({email}) notification for your reservation.", "ReservationConfirmation.reservationDetailsTitle": "Reservation details", "ReservationControls.cancel": "Cancel", "ReservationControls.confirm": "Approve", diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index 34a6981eb..8211fb745 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -160,7 +160,7 @@ "ReservationConfirmation.reservationEditedTitle": "Varaus päivitetty", "ReservationConfirmation.resourceButton": "Palaa tilan tietoihin", "ReservationConfirmation.ownReservationButton": "Palaa omiin varauksiin", - "ReservationConfirmation.confirmationText": "Saat sähköpostiisi ({email}) vahvistuksen varauksesta.", + "ReservationConfirmation.confirmationText": "Saat sähköpostiisi ({email}) muistutuksen varauksesta.", "ReservationConfirmation.reservationDetailsTitle": "Varauksen lisätiedot", "ReservationControls.cancel": "Peru", "ReservationControls.confirm": "Hyväksy", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index 5e018021c..1339e291f 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -162,7 +162,7 @@ "ReservationConfirmation.reservationEditedTitle": "Bokningen uppdaterad", "ReservationConfirmation.resourceButton": "Tillbaka till sökresultat", "ReservationConfirmation.ownReservationButton": "Visa mina bokningar", - "ReservationConfirmation.confirmationText": "Du får ett bekräftelsemeddelande till din ({email}) epostadress.", + "ReservationConfirmation.confirmationText": "Du får en påminnelse till din ({email}) epostadress.", "ReservationConfirmation.reservationDetailsTitle": "Bokningens detaljer", "ReservationControls.cancel": "Avbryt", "ReservationControls.confirm": "Godkänn", From 4b1868b9e94e5346d593d1ef5bf9b7518a9aecf4 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 10:53:47 +0300 Subject: [PATCH 21/56] add slot size functionality --- app/i18n/messages/en.json | 2 +- app/i18n/messages/fi.json | 2 +- app/i18n/messages/sv.json | 2 +- .../time-slots/TimeSlot.js | 1 + app/state/reducers/ui/reservationsReducer.js | 56 +++++++++++-------- app/utils/timeUtils.js | 41 ++++++++++++++ 6 files changed, 78 insertions(+), 26 deletions(-) diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index 3ead24636..bd3418515 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -123,7 +123,7 @@ "Notifications.loginToReserve": "Log in to reserve these premises.", "Notifications.reservationDeleteSuccessMessage": "Your reservation was successfully cancelled.", "Notifications.reservationUpdateSuccessMessage": "Your reservation has been updated.", - "Notifications.selectTimeToReserve.warning": "Mimimum time reservation is required, please select earlier time slot", + "Notifications.selectTimeToReserve.warning": "The reservation overlaps with the existing reservation or is outside the opening hours.", "Notifications.selectTimeToReserve": "Select the time for which you wish to make the reservation", "Partners.kaupunginkirjatoImageAlt": "Helsinki City Library", "Partners.nuorisoasiainkeskusImageAlt": "City of Helsinki – Youth Department", diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index ed1e03032..6617b941a 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -123,7 +123,7 @@ "Notifications.loginToReserve": "Kirjaudu sisään tehdäksesi varauksen tähän tilaan.", "Notifications.reservationDeleteSuccessMessage": "Varauksen peruminen onnistui.", "Notifications.reservationUpdateSuccessMessage": "Varaus päivitetty.", - "Notifications.selectTimeToReserve.warning": "The reservation overlaps with the existing reservation or is outside the opening hours.", + "Notifications.selectTimeToReserve.warning": "Varaus menee päällekkäin olemassa olevan varauksen kanssa tai se on aukioloaikojen ulkopuolella.", "Notifications.selectTimeToReserve": "Valitse aika, jolle haluat tehdä varauksen.", "Partners.kaupunginkirjatoImageAlt": "Helsingin kaupunginkirjasto", "Partners.nuorisoasiainkeskusImageAlt": "Helsingin kaupunki - nuorisoasiainkeskus", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index b03c0b00d..e7e5822dd 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -125,7 +125,7 @@ "Notifications.loginToReserve": "Logga in för att boka detta utrymme.", "Notifications.reservationDeleteSuccessMessage": "Bokningen avbokades.", "Notifications.reservationUpdateSuccessMessage": "Bokningen uppdaterades.", - "Notifications.selectTimeToReserve.warning": "The reservation overlaps with the existing reservation or is outside the opening hours.", + "Notifications.selectTimeToReserve.warning": "Bokningen överlappar med befintlig bokning eller ligger utanför öppettiderna.", "Notifications.selectTimeToReserve": "Välj en tidpunkt som du vill boka.", "Partners.kaupunginkirjatoImageAlt": "Helsingfors stadsbibliotek", "Partners.nuorisoasiainkeskusImageAlt": "Helsingfors stad - ungdomscentralen", diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js index c9583a9de..b6492fdd9 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js @@ -98,6 +98,7 @@ class TimeSlot extends PureComponent { begin: slot.start, end: slot.end, resource: resource.id, + minPeriod: resource.minPeriod }); } }; diff --git a/app/state/reducers/ui/reservationsReducer.js b/app/state/reducers/ui/reservationsReducer.js index 9df2def71..9077e6496 100644 --- a/app/state/reducers/ui/reservationsReducer.js +++ b/app/state/reducers/ui/reservationsReducer.js @@ -1,16 +1,14 @@ import types from 'constants/ActionTypes'; import ModalTypes from 'constants/ModalTypes'; -import find from 'lodash/find'; +import omit from 'lodash/omit'; +import isEmpty from 'lodash/isEmpty'; import first from 'lodash/first'; import last from 'lodash/last'; -import orderBy from 'lodash/orderBy'; -import without from 'lodash/without'; -import moment from 'moment'; import Immutable from 'seamless-immutable'; -import { getTimeSlots } from 'utils/timeUtils'; +import { getTimeSlots, getEndTimeSlotWithMinPeriod, getTimeDiff } from 'utils/timeUtils'; const initialState = Immutable({ adminReservationFilters: { @@ -136,26 +134,38 @@ function reservationsReducer(state = initialState, action) { } case types.UI.TOGGLE_TIME_SLOT: { - const slot = action.payload; - const stateSlot = find(state.selected, slot); - if (stateSlot) { - return state.merge({ selected: without(state.selected, stateSlot) }); - } if (state.selected.length <= 1) { - return state.merge({ selected: [...state.selected, slot] }); - } - const orderedSelected = orderBy(state.selected, 'begin'); - const firstSelected = first(orderedSelected); - const lastSelected = last(orderedSelected); - if (moment(lastSelected.begin).isBefore(slot.begin)) { - return state.merge({ selected: [...without(state.selected, lastSelected), slot] }); + const minPeriod = action.payload.minPeriod; + let minPeriodSlot; + + const startSlot = omit(action.payload, 'minPeriod'); + // Remove minPeriod of slot information from payload. + // startSlot is known as input slot from payload. + + if (isEmpty(state.selected)) { + // No time slot have been selected. + // auto append minPeriodSlot to selected state to make sure minPeriod time is selected. + // If minPeriod exist + minPeriodSlot = getEndTimeSlotWithMinPeriod(startSlot, minPeriod); + + return state.merge({ selected: [startSlot, minPeriod && minPeriodSlot] }); } - if ( - moment(firstSelected.begin).isBefore(slot.begin) - && moment(lastSelected.begin).isAfter(slot.begin) - ) { - return state.merge({ selected: [...without(state.selected, lastSelected), slot] }); + + if (!minPeriod) { + // no minPeriod, make startSlot as end timeslot + + return state.merge({ selected: [state.selected[0], startSlot] }); } - return state.merge({ selected: [...state.selected, slot] }); + + // startSlot > minPeriodSlot => payload slot is larger than minPeriod + // startSlot === minPeriodSlot => make one of them as end timeslot + // startSlot < minPeriodSlot => keep minPeriod as selected. + minPeriodSlot = getEndTimeSlotWithMinPeriod(state.selected[0], minPeriod); + const timeDiff = getTimeDiff(startSlot.begin, minPeriodSlot.begin); + + return state.merge({ + selected: [state.selected[0], + timeDiff > 0 ? startSlot : minPeriodSlot] + }); } case types.UI.CLEAR_TIME_SLOTS: { diff --git a/app/utils/timeUtils.js b/app/utils/timeUtils.js index 7dea9ea13..24f353cbc 100644 --- a/app/utils/timeUtils.js +++ b/app/utils/timeUtils.js @@ -182,6 +182,44 @@ function prettifyHours(hours, showMinutes = false) { function padLeft(number) { return number < 10 ? `0${number}` : String(number); } +/** + * Convert time period to minutes; + * + * @param {string} period Time string, usually HH:MM:SS + * @returns {Int} Period in minutes + */ +function periodToMinute(period) { + return moment.duration(period).asMinutes(); +} + +/** + * Get end time slot with minPeriod time range. + * For example: start slot at 2AM, minPeriod = 1h, expected result 3AM + * + * @param {object} startSlot + * @param {string} slotSize + * @param {string} minPeriod + * @return {object} endSlot + */ +function getEndTimeSlotWithMinPeriod(startSlot, minPeriod) { + const minPeriodInMinutes = periodToMinute(minPeriod); + + return { + resource: startSlot.resource, + begin: moment(startSlot.begin).add(minPeriodInMinutes, 'minutes').toISOString(), + end: moment(startSlot.end).add(minPeriodInMinutes, 'minutes').toISOString() + }; +} +/** + * Get time different + * + * @param {string} startTime ISO Time String + * @param {string} endTime ISO Time String + * @returns {int} timediff + */ +function getTimeDiff(startTime, endTime) { + return moment(startTime).diff(moment(endTime)); +} export { addToDate, @@ -197,4 +235,7 @@ export { isPastDate, prettifyHours, padLeft, + periodToMinute, + getEndTimeSlotWithMinPeriod, + getTimeDiff }; From f9cd2dfae73ffbd5ffb7eb586cef22fd5aa54e6d Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 11:12:46 +0300 Subject: [PATCH 22/56] add minPeriod info on resource page --- app/i18n/messages/en.json | 3 +++ app/i18n/messages/fi.json | 5 ++++- app/i18n/messages/sv.json | 3 +++ app/pages/resource/ResourcePage.js | 21 ++++++++++++++++++--- app/utils/resourceUtils.js | 10 ++++++++++ 5 files changed, 38 insertions(+), 4 deletions(-) diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index bd3418515..d46f4af0f 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -184,6 +184,7 @@ "ReservationInfo.loginMessage": "You must log in to reserve these premises.", "ReservationInfo.maxNumberOfReservations": "Maximum number of reservations per user:", "ReservationInfo.reservationMaxLength": "Maximum duration of the reservation:", + "ReservationInfo.reservationMinLength": "Minimum duration of the reservation:", "ReservationInfo.reservationEarliestDays": "Reservation at the earliest in {time}", "ReservationInfoModal.cancelButton": "Cancel event", "ReservationInfoModal.confirmButton": "Confirm reservation", @@ -228,6 +229,8 @@ "ResourceHeader.backButton": "Back to search results", "ResourceHeader.maxPeriodDays": "max {days} days", "ResourceHeader.maxPeriodHours": "max {hours} h", + "ResourceHeader.minPeriodDays": "min {days} days", + "ResourceHeader.minPeriodHours": "min {hours} h", "ResourceHeader.mapButton": "See position on map", "ResourceHeader.resourceButton": "See resource info", "ResourceHeader.favoriteAddButton": "Add favorite", diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index 6617b941a..ead90769f 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -183,7 +183,8 @@ "ReservationForm.termsAndConditionsLabel": "Olen lukenut ja hyväksynyt tilan käyttösäännöt", "ReservationInfo.loginMessage": "Sinun täytyy kirjautua sisään, jotta voit tehdä varauksen tähän tilaan.", "ReservationInfo.maxNumberOfReservations": "Maksimimäärä varauksia per käyttäjä:", - "ReservationInfo.reservationMaxLength": "Varauksen maksimipituus", + "ReservationInfo.reservationMaxLength": "Varauksen maksimipituus:", + "ReservationInfo.reservationMinLength": "Varauksen vähimmäispituus:", "ReservationInfo.reservationEarliestDays": "Varattavissa vähintään {time} etukäteen", "ReservationInfoModal.cancelButton": "Peru tapahtuma", "ReservationInfoModal.confirmButton": "Hyväksy tapahtuma", @@ -228,6 +229,8 @@ "ResourceHeader.backButton": "Takaisin hakutuloksiin", "ResourceHeader.maxPeriodDays": "max {days}pv", "ResourceHeader.maxPeriodHours": "max {hours}h", + "ResourceHeader.minPeriodDays": "min {days}pv", + "ResourceHeader.minPeriodHours": "min {hours}h", "ResourceHeader.mapButton": "Katso sijainti kartalta", "ResourceHeader.resourceButton": "Katso tilan tiedot", "ResourceHeader.favoriteAddButton": "Lisää suosikiksi", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index e7e5822dd..c0aed7349 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -186,6 +186,7 @@ "ReservationInfo.loginMessage": "För att kunna boka detta utrymme måste du logga in.", "ReservationInfo.maxNumberOfReservations": "Maximalt antal bokningar per användare:", "ReservationInfo.reservationMaxLength": "Bokningens maximala längd:", + "ReservationInfo.reservationMinLength": "Minsta längd av bokningen:", "ReservationInfo.reservationEarliestDays": "Bokning tidigast inom {time}", "ReservationInfoModal.cancelButton": "Annulera evenmanget", "ReservationInfoModal.confirmButton": "Bekräfta reserveringen", @@ -230,6 +231,8 @@ "ResourceHeader.backButton": "Tillbaka till sökresultat", "ResourceHeader.maxPeriodDays": "max {days} dag", "ResourceHeader.maxPeriodHours": "max {hours}h", + "ResourceHeader.minPeriodDays": "min {days} dag", + "ResourceHeader.minPeriodHours": "min {hours}h", "ResourceHeader.mapButton": "Läget på kartan", "ResourceHeader.resourceButton": "Utrymmets uppgifter", "ResourceHeader.favoriteAddButton": "Spara som favorit", diff --git a/app/pages/resource/ResourcePage.js b/app/pages/resource/ResourcePage.js index 416f231f3..a7de99930 100644 --- a/app/pages/resource/ResourcePage.js +++ b/app/pages/resource/ResourcePage.js @@ -19,7 +19,7 @@ import NotFoundPage from 'pages/not-found/NotFoundPage'; import ResourceCalendar from 'shared/resource-calendar'; import ResourceMap from 'shared/resource-map'; import { injectT } from 'i18n'; -import { getMaxPeriodText, getResourcePageUrl } from 'utils/resourceUtils'; +import { getMaxPeriodText, getResourcePageUrl, getMinPeriodText } from 'utils/resourceUtils'; import ReservationCalendar from './reservation-calendar'; import ResourceHeader from './resource-header'; import ResourceInfo from './resource-info'; @@ -145,7 +145,7 @@ class UnconnectedResourcePage extends Component { } const maxPeriodText = getMaxPeriodText(t, resource); - + const minPeriodText = getMinPeriodText(t, resource); const images = this.orderImages(resource.images || []); const mainImageIndex = findIndex(images, image => image.type === 'main'); @@ -195,7 +195,22 @@ class UnconnectedResourcePage extends Component { )} {!resource.externalReservationUrl && (
- {`${t('ReservationInfo.reservationMaxLength')} ${maxPeriodText}`} + {/* Show reservation max period text */} + {resource.maxPeriod && ( +
+ {`${t('ReservationInfo.reservationMaxLength')} ${maxPeriodText}`} +
+ )} + + {/* Show reservation max period text */} + {resource.minPeriod + && ( +
+

{`${t('ReservationInfo.reservationMinLength')} ${minPeriodText}`}

+
+ ) + } + 0) { + return t('ResourceHeader.minPeriodDays', { days }); + } + return t('ResourceHeader.minPeriodHours', { hours }); +} + function getOpeningHours(resource, selectedDate) { if (resource && resource.openingHours && resource.openingHours.length) { if (selectedDate) { @@ -216,4 +225,5 @@ export { getResourcePageUrlComponents, getTermsAndConditions, reservingIsRestricted, + getMinPeriodText }; From a7ba7725f929afc95544c5b50c2ca2e6f7a2512e Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 11:58:15 +0300 Subject: [PATCH 23/56] fix last slot to be last selectable slot --- .../resource/reservation-calendar/time-slots/TimeSlots.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js index 5d5f9eb69..8ea6256f5 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js @@ -149,7 +149,9 @@ class TimeSlots extends Component { return null; } const slot = timeSlots[0]; - const lastSlot = timeSlots[timeSlots.length - 1]; + const lastSlot = timeSlots[timeSlots.length - 2]; + // last slot is selectable slot, which is slot before last slot in array + const placeholderSize = timeSlotPlaceholderSizes[index]; const slotDate = moment(slot.start).format(constants.DATE_FORMAT); From 9816d956816db231f24c9668bd19fd91b9c589b9 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 14:26:38 +0300 Subject: [PATCH 24/56] rename slot_size to slotSize to match api --- .../reservationCalendarSelector.js | 2 +- .../reservationCalendarSelector.spec.js | 6 +++--- .../reservation-calendar/time-slots/TimeSlots.js | 12 +++++------- app/pages/resource/reservation-calendar/utils.js | 3 +++ .../modals/reservation-info/ReservationEditForm.js | 2 +- .../ReservationControlsContainer.js | 2 +- .../ReservationControlsContainer.spec.js | 2 +- .../resourceCalendarSelector.spec.js | 2 +- app/utils/fixtures/Resource.js | 2 +- 9 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/pages/resource/reservation-calendar/reservationCalendarSelector.js b/app/pages/resource/reservation-calendar/reservationCalendarSelector.js index 6b3951806..a70789b94 100644 --- a/app/pages/resource/reservation-calendar/reservationCalendarSelector.js +++ b/app/pages/resource/reservation-calendar/reservationCalendarSelector.js @@ -77,7 +77,7 @@ const timeSlotsSelector = createSelector( toEditSelector, (resourceDates, reservationsToEdit) => resourceDates.map((resource) => { const { closes, opens } = getOpeningHours(resource); - const period = resource.slot_size || DEFAULT_SLOT_SIZE; + const period = resource.slotSize || DEFAULT_SLOT_SIZE; const reservations = getOpenReservations(resource); const timeSlots = getTimeSlots(opens, closes, period, reservations, reservationsToEdit); if (timeSlots.length) { diff --git a/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js b/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js index ab3b9f07b..b01b99ff4 100644 --- a/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js +++ b/app/pages/resource/reservation-calendar/reservationCalendarSelector.spec.js @@ -48,7 +48,7 @@ function getProps(id = 'some-id', date = '2015-10-10') { describe('pages/resource/reservation-calendar/reservationCalendarSelector', () => { const resource = Resource.build({ availableHours, - slot_size: '01:00:00', + slotSize: '01:00:00', openingHours: openingHoursMonth, reservations: [ { @@ -163,7 +163,7 @@ describe('pages/resource/reservation-calendar/reservationCalendarSelector', () = const actualArgs = timeUtils.getTimeSlots.calls[5].args; expect(actualArgs[0]).toBe('2015-10-10T12:00:00+03:00'); expect(actualArgs[1]).toBe('2015-10-10T18:00:00+03:00'); - expect(actualArgs[2]).toBe(resource.slot_size); + expect(actualArgs[2]).toBe(resource.slotSize); expect(actualArgs[3]).toEqual(resource.reservations); expect(selected.timeSlots).toEqual(expectedMockSlots); simple.restore(); @@ -190,7 +190,7 @@ describe('pages/resource/reservation-calendar/reservationCalendarSelector', () = const actualArgs = timeUtils.getTimeSlots.calls[5].args; expect(actualArgs[0]).toBe('2015-10-10T12:00:00+03:00'); expect(actualArgs[1]).toBe('2015-10-10T18:00:00+03:00'); - expect(actualArgs[2]).toBe(resource.slot_size); + expect(actualArgs[2]).toBe(resource.slotSize); expect(actualArgs[3]).toEqual(resource.reservations); expect(selected.timeSlots).toEqual(expectedMockSlots); simple.restore(); diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js index 8ea6256f5..c50d723b8 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js @@ -50,18 +50,12 @@ class TimeSlots extends Component { }; onMouseEnter = (hoveredTimeSlot) => { - if (this.props.selected.length !== 1) { - return; - } this.setState(() => ({ hoveredTimeSlot, })); }; onMouseLeave = () => { - if (this.props.selected.length !== 1) { - return; - } this.setState(() => ({ hoveredTimeSlot: null, })); @@ -219,8 +213,12 @@ class TimeSlots extends Component { isAdmin ); const isSelected = utils.isSlotSelected(slot, selected); + const isHoveredSlotSelected = utils.isSlotSelected(hoveredTimeSlot, selected); + const isFirstSelected = utils.isFirstSelected(slot, selected); - const shouldShowReservationPopover = selected.length === 1 && isFirstSelected; + const shouldShowReservationPopover = hoveredTimeSlot + && isFirstSelected && !isHoveredSlotSelected; + const isHighlighted = utils.isHighlighted(slot, selected, hoveredTimeSlot); const resBegin = this.getReservationBegin(); const resEnd = this.getReservationEnd(); diff --git a/app/pages/resource/reservation-calendar/utils.js b/app/pages/resource/reservation-calendar/utils.js index a84f01109..7d1d5f6d5 100644 --- a/app/pages/resource/reservation-calendar/utils.js +++ b/app/pages/resource/reservation-calendar/utils.js @@ -128,6 +128,9 @@ function isHighlighted(slot, selected, hovered) { */ function isUnderMinPeriod(selected, slot, lastSlot, minPeriod) { let isUnder = false; + if (!slot.end || !lastSlot.end) { + return isUnder; + } if (!selected.length && minPeriod) { const minPeriodInMinutes = moment.duration(minPeriod).asMinutes(); diff --git a/app/shared/modals/reservation-info/ReservationEditForm.js b/app/shared/modals/reservation-info/ReservationEditForm.js index f103461fa..95a6f5bcc 100644 --- a/app/shared/modals/reservation-info/ReservationEditForm.js +++ b/app/shared/modals/reservation-info/ReservationEditForm.js @@ -109,7 +109,7 @@ class UnconnectedReservationEditForm extends Component { diff --git a/app/shared/reservation-controls/ReservationControlsContainer.js b/app/shared/reservation-controls/ReservationControlsContainer.js index 3e814f269..da87d70d7 100644 --- a/app/shared/reservation-controls/ReservationControlsContainer.js +++ b/app/shared/reservation-controls/ReservationControlsContainer.js @@ -58,7 +58,7 @@ export class UnconnectedReservationControlsContainer extends Component { } = this.props; const nextUrl = getEditReservationUrl(reservation); - actions.selectReservationToEdit({ reservation, slotSize: resource.slot_size }); + actions.selectReservationToEdit({ reservation, slotSize: resource.slotSize }); history.push(nextUrl); } diff --git a/app/shared/reservation-controls/ReservationControlsContainer.spec.js b/app/shared/reservation-controls/ReservationControlsContainer.spec.js index 3d8cc4d42..07b9067b4 100644 --- a/app/shared/reservation-controls/ReservationControlsContainer.spec.js +++ b/app/shared/reservation-controls/ReservationControlsContainer.spec.js @@ -95,7 +95,7 @@ describe('shared/reservation-controls/ReservationControlsContainer', () => { expect(props.actions.selectReservationToEdit.callCount).toBe(1); expect(props.actions.selectReservationToEdit.lastCall.args[0]).toEqual({ reservation: props.reservation, - slotSize: props.resource.slot_size, + slotSize: props.resource.slotSize, }); } ); diff --git a/app/shared/resource-calendar/resourceCalendarSelector.spec.js b/app/shared/resource-calendar/resourceCalendarSelector.spec.js index ee70533a3..2cbc8d368 100644 --- a/app/shared/resource-calendar/resourceCalendarSelector.spec.js +++ b/app/shared/resource-calendar/resourceCalendarSelector.spec.js @@ -43,7 +43,7 @@ describe('shared/resource-calendar/resourceCalendarSelector', () => { ends: '2015-10-11T20:00:00+03:00', }, ], - slot_size: '01:00:00', + slotSize: '01:00:00', openingHours: openingHoursMonth, reservations: [ // Day 2015-10-10 is partially available diff --git a/app/utils/fixtures/Resource.js b/app/utils/fixtures/Resource.js index 9d958d54a..7800d5d28 100644 --- a/app/utils/fixtures/Resource.js +++ b/app/utils/fixtures/Resource.js @@ -17,5 +17,5 @@ const Resource = new Factory() .attr('supportedReservationExtraFields', []) .attr('userPermissions', { isAdmin: false, canMakeReservations: true }) .attr('isFavorite', false) - .attr('slot_size', DEFAULT_SLOT_SIZE); + .attr('slotSize', DEFAULT_SLOT_SIZE); export default Resource; From 95cd1ed536413f76ea18a81c437621fd6ba4fb0c Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 15:39:37 +0300 Subject: [PATCH 25/56] fix unit tests --- .../resource/reservation-calendar/utils.js | 11 +++++++---- app/state/reducers/ui/reservationsReducer.js | 4 ++-- .../reducers/ui/reservationsReducer.spec.js | 18 +----------------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/app/pages/resource/reservation-calendar/utils.js b/app/pages/resource/reservation-calendar/utils.js index 7d1d5f6d5..71a7b7695 100644 --- a/app/pages/resource/reservation-calendar/utils.js +++ b/app/pages/resource/reservation-calendar/utils.js @@ -127,17 +127,20 @@ function isHighlighted(slot, selected, hovered) { * @returns */ function isUnderMinPeriod(selected, slot, lastSlot, minPeriod) { - let isUnder = false; + if (!selected || !slot || !lastSlot) { + return false; + } + if (!slot.end || !lastSlot.end) { - return isUnder; + return false; } if (!selected.length && minPeriod) { const minPeriodInMinutes = moment.duration(minPeriod).asMinutes(); - isUnder = moment(slot.start).add(minPeriodInMinutes, 'minutes') > (moment(lastSlot.end)); + return moment(slot.start).add(minPeriodInMinutes, 'minutes') > (moment(lastSlot.end)); } - return isUnder; + return false; } export default { diff --git a/app/state/reducers/ui/reservationsReducer.js b/app/state/reducers/ui/reservationsReducer.js index 9077e6496..e05466463 100644 --- a/app/state/reducers/ui/reservationsReducer.js +++ b/app/state/reducers/ui/reservationsReducer.js @@ -145,9 +145,9 @@ function reservationsReducer(state = initialState, action) { // No time slot have been selected. // auto append minPeriodSlot to selected state to make sure minPeriod time is selected. // If minPeriod exist - minPeriodSlot = getEndTimeSlotWithMinPeriod(startSlot, minPeriod); + minPeriodSlot = minPeriod && getEndTimeSlotWithMinPeriod(startSlot, minPeriod); - return state.merge({ selected: [startSlot, minPeriod && minPeriodSlot] }); + return state.merge({ selected: minPeriod ? [startSlot, minPeriodSlot] : [startSlot] }); } if (!minPeriod) { diff --git a/app/state/reducers/ui/reservationsReducer.spec.js b/app/state/reducers/ui/reservationsReducer.spec.js index 0ebe158e3..c852df55e 100644 --- a/app/state/reducers/ui/reservationsReducer.spec.js +++ b/app/state/reducers/ui/reservationsReducer.spec.js @@ -542,22 +542,6 @@ describe('state/reducers/ui/reservationsReducer', () => { }); describe('if slot is already selected', () => { - test('removes the given slot from selected', () => { - const slot = { - begin: '2015-10-11T10:00:00Z', - end: '2015-10-11T11:00:00Z', - resource: 'some-resource', - }; - const action = toggleTimeSlot(slot); - const initialState = Immutable({ - selected: [slot], - }); - const nextState = reservationsReducer(initialState, action); - const expected = Immutable([]); - - expect(nextState.selected).toEqual(expected); - }); - test('does not affect other selected slots ', () => { const slot1 = { begin: '2015-12-12T10:00:00Z', @@ -574,7 +558,7 @@ describe('state/reducers/ui/reservationsReducer', () => { selected: [slot1, slot2], }); const nextState = reservationsReducer(initialState, action); - const expected = Immutable([slot1]); + const expected = Immutable([slot1, slot2]); expect(nextState.selected).toEqual(expected); }); From fde62417d03363911b66f38e5517ff0e5f1c3045 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 16:25:32 +0300 Subject: [PATCH 26/56] fix weird placeholder broke rendering --- .../resource/reservation-calendar/time-slots/TimeSlots.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js index c50d723b8..c0a23cb3a 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js @@ -7,6 +7,7 @@ import moment from 'moment'; import classnames from 'classnames'; import findIndex from 'lodash/findIndex'; import minBy from 'lodash/minBy'; +import round from 'lodash/round'; import { injectT } from 'i18n'; import ReservationPopover from 'shared/reservation-popover'; @@ -87,7 +88,7 @@ class TimeSlots extends Component { .map(timeslots => timeslots && timeslots[0]) .filter(value => !!value && value.end); const slotLength = firstTimeSlots[0] - ? moment(firstTimeSlots[0].end).diff(firstTimeSlots[0].start, 'm') + ? moment(firstTimeSlots[0].end).diff(firstTimeSlots[0].start, 'minutes') : constants.TIME_SLOT_DEFAULT_LENGTH; if (firstTimeSlots.length === 0) { @@ -106,7 +107,8 @@ class TimeSlots extends Component { return null; } const currentStart = moment(slot[0].start).set(dateForTimeComparison); - return currentStart.diff(earliestStart, 'minutes') / slotLength; + return round(currentStart.diff(earliestStart, 'minutes') / slotLength); + // TODO: Please fix me, i have no idea what im doing. }); const selectedDateIndex = findIndex( From 9ccd60dc4fffb73394b92a43d009f77da08d6d67 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 16:32:58 +0300 Subject: [PATCH 27/56] ignore minPeriod config if equal slotSize, fix alignments --- app/pages/resource/ResourcePage.js | 12 ++++++------ .../reservation-calendar/time-slots/TimeSlot.js | 3 ++- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/app/pages/resource/ResourcePage.js b/app/pages/resource/ResourcePage.js index a7de99930..ff09d0760 100644 --- a/app/pages/resource/ResourcePage.js +++ b/app/pages/resource/ResourcePage.js @@ -197,17 +197,17 @@ class UnconnectedResourcePage extends Component {
{/* Show reservation max period text */} {resource.maxPeriod && ( -
- {`${t('ReservationInfo.reservationMaxLength')} ${maxPeriodText}`} -
+
+ {`${t('ReservationInfo.reservationMaxLength')} ${maxPeriodText}`} +
)} {/* Show reservation max period text */} {resource.minPeriod && ( -
-

{`${t('ReservationInfo.reservationMinLength')} ${minPeriodText}`}

-
+
+

{`${t('ReservationInfo.reservationMinLength')} ${minPeriodText}`}

+
) } diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js index b6492fdd9..9b095328e 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js @@ -98,7 +98,8 @@ class TimeSlot extends PureComponent { begin: slot.start, end: slot.end, resource: resource.id, - minPeriod: resource.minPeriod + minPeriod: resource.minPeriod === resource.slotSize ? null : resource.minPeriod + // Ignore minPeriod if minPeriod is equal slotSize }); } }; From 7771b78f912fc948924e7d82d97933ea03f4c544 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 17:16:00 +0300 Subject: [PATCH 28/56] add unit tests for timeUtils funcs --- app/utils/__tests__/timeUtils.spec.js | 58 +++++++++++++++++++++++++++ app/utils/timeUtils.js | 7 ++-- 2 files changed, 62 insertions(+), 3 deletions(-) diff --git a/app/utils/__tests__/timeUtils.spec.js b/app/utils/__tests__/timeUtils.spec.js index 9f59b2b87..d5f5f7f70 100644 --- a/app/utils/__tests__/timeUtils.spec.js +++ b/app/utils/__tests__/timeUtils.spec.js @@ -16,9 +16,12 @@ import { getEndTimeString, getStartTimeString, getTimeSlots, + getTimeDiff, isPastDate, padLeft, prettifyHours, + periodToMinute, + getEndTimeSlotWithMinPeriod } from 'utils/timeUtils'; const moment = extendMoment(Moment); @@ -615,4 +618,59 @@ describe('Utils: timeUtils', () => { }); }); }); + + describe('getTimeDiff', () => { + test('return timediff in number by default', () => { + const startDate = '2019-05-09T05:00:01.000Z'; + const endDate = '2019-05-09T05:00:00.000Z'; + + const expected = 1000; + + expect(getTimeDiff(startDate, endDate)).toEqual(expected); + }); + + test('return timediff in defined unit', () => { + const startDate = '2019-05-09T05:30:01.000Z'; + const endDate = '2019-05-09T05:00:00.000Z'; + const unit = 'minutes'; + const expected = 30; + + expect(getTimeDiff(startDate, endDate, unit)).toEqual(expected); + }); + + test('can be used to compare time', () => { + const startDate = '2019-05-09T05:30:00.000Z'; + const endDate = '2019-05-09T05:00:00.000Z'; + + // > 0 => startTime > endTime + expect(getTimeDiff(startDate, endDate) > 0).toBeTruthy(); + }); + }); + + describe('getEndTimeSlotWithMinPeriod', () => { + test('return end time slot with timediff equal minPeriod', () => { + const slot = { + begin: '2019-05-09T05:00:00.000Z', + end: '2019-05-09T05:30:00.000Z', + resource: 'abc' + }; + + const minPeriod = '01:00:00'; + const result = getEndTimeSlotWithMinPeriod(slot, minPeriod); + + expect(getTimeDiff(result.begin, slot.begin, 'minutes')).toEqual(periodToMinute(minPeriod)); + expect(getTimeDiff(result.end, slot.end, 'minutes')).toEqual(periodToMinute(minPeriod)); + expect(result.resource).toEqual(slot.resource); + }); + }); + + describe('periodToMinute', () => { + test('convert time period to minutes', () => { + const period = '01:00:00'; + + const result = periodToMinute(period); + + expect(result).toEqual(60); + }); + }); }); diff --git a/app/utils/timeUtils.js b/app/utils/timeUtils.js index 24f353cbc..b7ecacb65 100644 --- a/app/utils/timeUtils.js +++ b/app/utils/timeUtils.js @@ -210,15 +210,16 @@ function getEndTimeSlotWithMinPeriod(startSlot, minPeriod) { end: moment(startSlot.end).add(minPeriodInMinutes, 'minutes').toISOString() }; } + /** * Get time different - * + * This function can be use to compare time * @param {string} startTime ISO Time String * @param {string} endTime ISO Time String * @returns {int} timediff */ -function getTimeDiff(startTime, endTime) { - return moment(startTime).diff(moment(endTime)); +function getTimeDiff(startTime, endTime, unit) { + return moment(startTime).diff(moment(endTime), unit); } export { From 789fbc26f50566136503c630878a11437e85fac3 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 8 May 2019 17:45:38 +0300 Subject: [PATCH 29/56] add unit test for time slot reducer --- .../reducers/ui/reservationsReducer.spec.js | 127 +++++++++++++++++- 1 file changed, 123 insertions(+), 4 deletions(-) diff --git a/app/state/reducers/ui/reservationsReducer.spec.js b/app/state/reducers/ui/reservationsReducer.spec.js index c852df55e..f11e6ab08 100644 --- a/app/state/reducers/ui/reservationsReducer.spec.js +++ b/app/state/reducers/ui/reservationsReducer.spec.js @@ -442,8 +442,127 @@ describe('state/reducers/ui/reservationsReducer', () => { }); describe('UI.TOGGLE_TIME_SLOT', () => { + describe('minPeriod', () => { + test('from payload is omit from slot data, add start slot to selected normally if minPeriod is undefined', () => { + const initialState = Immutable({ + selected: [], + }); + const slot = { + begin: '2015-10-11T10:00:00Z', + end: '2015-10-11T11:00:00Z', + resource: 'some-resource', + }; + const action = toggleTimeSlot(slot); + const nextState = reservationsReducer(initialState, action); + const expected = Immutable([slot]); + + expect(nextState.selected).toEqual(expected); + expect(expected).not.toHaveProperty('minPeriod'); + }); + + test('auto populate start time slot with end time slot, range between equal minPeriod', () => { + const initialState = Immutable({ + selected: [] + }); + const minPeriod = '01:00:00'; + const startTimeSlot = { + begin: '2019-05-09T05:00:00.000Z', + end: '2019-05-09T06:00:00.000Z', + resource: 'some-resource', + }; + const payload = { + ...startTimeSlot, + minPeriod + }; + + const expectedEndSlot = { + begin: '2019-05-09T06:00:00.000Z', + end: '2019-05-09T07:00:00.000Z', + resource: 'some-resource', + }; + const action = toggleTimeSlot(payload); + const nextState = reservationsReducer(initialState, action); + const expected = Immutable([startTimeSlot, expectedEndSlot]); + + expect(nextState.selected).toEqual(expected); + }); + + test('push new end time slot if its larger than current end time slot', () => { + const newEndTimeSlot = { + begin: '2019-05-09T08:00:00.000Z', + end: '2019-05-09T09:00:00.000Z', + resource: 'some-resource', + }; + + const originalStartTimeSlot = { + begin: '2019-05-09T05:00:00.000Z', + end: '2019-05-09T06:00:00.000Z', + resource: 'some-resource', + }; + const initialState = Immutable({ + selected: [ + originalStartTimeSlot, + { + begin: '2019-05-09T06:00:00.000Z', + end: '2019-05-09T07:00:00.000Z', + resource: 'some-resource', + }, + ] + }); + const minPeriod = '01:00:00'; + + const payload = { + ...newEndTimeSlot, + minPeriod + }; + + const action = toggleTimeSlot(payload); + const nextState = reservationsReducer(initialState, action); + const expected = Immutable([originalStartTimeSlot, newEndTimeSlot]); + + expect(nextState.selected).toEqual(expected); + }); + + test('in case new end time is less than minPeriod, keep end time slot to minPeriod', () => { + const newEndTimeSlot = { + begin: '2019-05-09T06:00:00.000Z', + end: '2019-05-09T07:00:00.000Z', + resource: 'some-resource', + }; + // 1h diff from original start time slot. 2h is minPeriod, not allowed to change + + const originalStartTimeSlot = { + begin: '2019-05-09T05:00:00.000Z', + end: '2019-05-09T06:00:00.000Z', + resource: 'some-resource', + }; + const generatedEndTimeSlot = { + begin: '2019-05-09T07:00:00.000Z', + end: '2019-05-09T08:00:00.000Z', + resource: 'some-resource', + }; + const initialState = Immutable({ + selected: [ + originalStartTimeSlot, + generatedEndTimeSlot, + ] + }); + const minPeriod = '02:00:00'; + + const payload = { + ...newEndTimeSlot, + minPeriod + }; + + const action = toggleTimeSlot(payload); + const nextState = reservationsReducer(initialState, action); + const expected = Immutable([originalStartTimeSlot, generatedEndTimeSlot]); + + expect(nextState.selected).toEqual(expected); + }); + }); describe('if slot is not already selected', () => { - test('adds the given slot to selected', () => { + test('adds the given slot to selected, known as start time slot', () => { const initialState = Immutable({ selected: [], }); @@ -459,7 +578,7 @@ describe('state/reducers/ui/reservationsReducer', () => { expect(nextState.selected).toEqual(expected); }); - test('does not affect other selected slots ', () => { + test('does not affect other selected slots, add 2nd time slot, known as end time slot ', () => { const initialState = Immutable({ selected: [ { @@ -482,7 +601,7 @@ describe('state/reducers/ui/reservationsReducer', () => { }); test( - 'replaces selected end slot when end slot after selected end slot', + 'replaces selected end slot with new end slot', () => { const initialState = Immutable({ selected: [ @@ -542,7 +661,7 @@ describe('state/reducers/ui/reservationsReducer', () => { }); describe('if slot is already selected', () => { - test('does not affect other selected slots ', () => { + test('duplicated will not affect other selected slots ', () => { const slot1 = { begin: '2015-12-12T10:00:00Z', end: '2015-12-12T11:00:00Z', From e5b7b8a68d81995e6a3c64bdf5aecbb5fa951af5 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Thu, 9 May 2019 11:33:03 +0300 Subject: [PATCH 30/56] add unit test for getMinPeriodText --- app/utils/__tests__/resourceUtils.spec.js | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/app/utils/__tests__/resourceUtils.spec.js b/app/utils/__tests__/resourceUtils.spec.js index a9707c107..fa84b8745 100644 --- a/app/utils/__tests__/resourceUtils.spec.js +++ b/app/utils/__tests__/resourceUtils.spec.js @@ -18,6 +18,7 @@ import { getTermsAndConditions, reservingIsRestricted, getResourcePageUrlComponents, + getMinPeriodText } from 'utils/resourceUtils'; describe('Utils: resourceUtils', () => { @@ -633,6 +634,30 @@ describe('Utils: resourceUtils', () => { }); }); + describe('getMinPeriodText', () => { + test('returns min period as days', () => { + const t = simple.stub().returnWith('days'); + const resource = { minPeriod: '24:00:00' }; + const result = getMinPeriodText(t, resource); + + expect(t.callCount).toBe(1); + expect(t.lastCall.args[0]).toEqual('ResourceHeader.minPeriodDays'); + expect(t.lastCall.args[1]).toEqual({ days: 1 }); + expect(result).toBe('days'); + }); + + test('returns min period as hours', () => { + const t = simple.stub().returnWith('hours'); + const resource = { minPeriod: '02:00:00' }; + const result = getMinPeriodText(t, resource); + + expect(t.callCount).toBe(1); + expect(t.lastCall.args[0]).toEqual('ResourceHeader.minPeriodHours'); + expect(t.lastCall.args[1]).toEqual({ hours: 2 }); + expect(result).toBe('hours'); + }); + }); + describe('getOpeningHours', () => { test('returns an empty object if given resource is undefined', () => { const resource = undefined; From 7975cdeea2811cf95d9237aa426f907e1300a8d4 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Thu, 9 May 2019 11:39:24 +0300 Subject: [PATCH 31/56] add more test for --- app/pages/resource/reservation-calendar/utils.spec.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/pages/resource/reservation-calendar/utils.spec.js b/app/pages/resource/reservation-calendar/utils.spec.js index 346e4a55e..31173a605 100644 --- a/app/pages/resource/reservation-calendar/utils.spec.js +++ b/app/pages/resource/reservation-calendar/utils.spec.js @@ -219,6 +219,15 @@ describe('pages/resource/reservation-calendar/utils', () => { expect(actual).toBe(false); }); + test('return false if slot have no end property (closed one)', () => { + const closeSlot = { + start: '2015-10-10T15:00:00Z', + }; + + const actual = utils.isUnderMinPeriod(selected, slot, closeSlot, minPeriod); + expect(actual).toBe(false); + }); + test('return true if selected slot will not fulfill minPeriod', () => { const actual = utils.isUnderMinPeriod(selected, inValidSlot, lastSlot, minPeriod); expect(actual).toBe(false); From 1fa613df97c7c4b3e086da589cdf5e5ca381f71a Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Thu, 9 May 2019 11:40:02 +0300 Subject: [PATCH 32/56] add generic tests for isUnderMinPeriod util function --- app/pages/resource/reservation-calendar/utils.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/pages/resource/reservation-calendar/utils.spec.js b/app/pages/resource/reservation-calendar/utils.spec.js index 31173a605..bed01d86e 100644 --- a/app/pages/resource/reservation-calendar/utils.spec.js +++ b/app/pages/resource/reservation-calendar/utils.spec.js @@ -204,6 +204,16 @@ describe('pages/resource/reservation-calendar/utils', () => { start: '2015-10-10T14:30:00Z', end: '2015-10-10T15:00:00Z', }; + + test('return false if required data is missing, wont throw error', () => { + const noLastSlot = utils.isUnderMinPeriod(selected, slot, undefined, minPeriod); + const noStartSlot = utils.isUnderMinPeriod(selected, undefined, lastSlot, minPeriod); + const noSelected = utils.isUnderMinPeriod(undefined, slot, lastSlot, minPeriod); + + expect(noLastSlot).toBeFalsy(); + expect(noStartSlot).toBeFalsy(); + expect(noSelected).toBeFalsy(); + }); test('returns false if minPeriod is not defined', () => { const actual = utils.isUnderMinPeriod(selected); expect(actual).toBe(false); From 0a5e5e1dc5d50dc1b53e7b5b7accd256113764b6 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Thu, 9 May 2019 11:48:59 +0300 Subject: [PATCH 33/56] add unit test for another case of isUnderMinPeriod --- app/pages/resource/reservation-calendar/utils.spec.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/pages/resource/reservation-calendar/utils.spec.js b/app/pages/resource/reservation-calendar/utils.spec.js index bed01d86e..c013c50cf 100644 --- a/app/pages/resource/reservation-calendar/utils.spec.js +++ b/app/pages/resource/reservation-calendar/utils.spec.js @@ -242,5 +242,15 @@ describe('pages/resource/reservation-calendar/utils', () => { const actual = utils.isUnderMinPeriod(selected, inValidSlot, lastSlot, minPeriod); expect(actual).toBe(false); }); + + test('return false if selected slot fulfill minPeriod', () => { + const validSlot = { + start: '2015-10-10T14:00:00Z', + end: '2015-10-10T14:30:00Z', + }; + + const actual = utils.isUnderMinPeriod(selected, validSlot, lastSlot, minPeriod); + expect(actual).toBe(false); + }); }); }); From 2bb4bdfbe30e7268ee9b5f485653bfed0a431cb8 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 9 May 2019 16:10:42 +0300 Subject: [PATCH 34/56] Translate SelectControl noOptionsMessage --- app/i18n/messages/en.json | 1 + app/i18n/messages/fi.json | 1 + app/i18n/messages/sv.json | 1 + app/pages/search/controls/SelectControl.js | 3 +++ 4 files changed, 6 insertions(+) diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index 69d4d22e8..a04dea7a4 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -67,6 +67,7 @@ "common.select": "Select", "common.userNameLabel": "Account name", "common.userEmailLabel": "Account email", + "SelectControl.noOptions": "No options", "ConfirmReservationModal.beforeText": "Original reservation time:", "ConfirmReservationModal.editTitle": "Changing reservation", "ConfirmReservationModal.formInfo": "Please fill in the following data for your preliminary reservation. The fields marked with an asterisk (*) are mandatory.", diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index d5c0aade2..b4e3c89f6 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -67,6 +67,7 @@ "common.select": "Valitse", "common.userNameLabel": "Tilin nimi", "common.userEmailLabel": "Tilin sähköposti", + "SelectControl.noOptions": "Ei valintoja", "ConfirmReservationModal.beforeText": "Alkuperäinen varausaika:", "ConfirmReservationModal.editTitle": "Varauksen muuttaminen", "ConfirmReservationModal.formInfo": "Täytä vielä seuraavat tiedot alustavaa varausta varten. Tähdellä (*) merkityt tiedot ovat pakollisia.", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index 38d1aa03d..e2b3e070e 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -67,6 +67,7 @@ "common.select": "Välj", "common.userNameLabel": "Konto namn", "common.userEmailLabel": "Konto e-post", + "SelectControl.noOptions": "Inga val", "ConfirmReservationModal.beforeText": "Ursprunglig bokningstid", "ConfirmReservationModal.editTitle": "Ändra bokningen", "ConfirmReservationModal.formInfo": "Fyll ännu i följande uppgifter för den preliminära bokningen. Uppgifterna markerade med en asterisk (*) är obligatoriska.", diff --git a/app/pages/search/controls/SelectControl.js b/app/pages/search/controls/SelectControl.js index 726bf8600..d99a5f1ab 100644 --- a/app/pages/search/controls/SelectControl.js +++ b/app/pages/search/controls/SelectControl.js @@ -17,6 +17,8 @@ class SelectControl extends React.Component { return options.find(option => option.value === value); }; + noOptionsMessage = () => this.props.t('SelectControl.noOptions') + render() { const { id, @@ -46,6 +48,7 @@ class SelectControl extends React.Component { isClearable={isClearable} isMulti={isMulti} isSearchable={isSearchable} + noOptionsMessage={this.noOptionsMessage} onChange={(selected, { action }) => { switch (action) { case 'clear': From 2970b74ce2fc0b30b2de2e661bf451bcd33fe8e8 Mon Sep 17 00:00:00 2001 From: Jimi Date: Fri, 10 May 2019 10:54:05 +0300 Subject: [PATCH 35/56] Add unit tests for noOptions text --- app/pages/search/controls/SelectControl.spec.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/app/pages/search/controls/SelectControl.spec.js b/app/pages/search/controls/SelectControl.spec.js index beb979a96..8180c187f 100644 --- a/app/pages/search/controls/SelectControl.spec.js +++ b/app/pages/search/controls/SelectControl.spec.js @@ -153,4 +153,18 @@ describe('pages/search/controls/SelectControl', () => { expect(selectedOption).toEqual(defaults.options); }); }); + + describe('displays noOptions-text when no options available', () => { + test('no options at all', () => { + const wrapper = getWrapper({ menuIsOpen: true, options: [] }); + const text = wrapper.render().text(); + expect(text).toContain('SelectControl.noOptions'); + }); + + test('selected all values in multi', () => { + const wrapper = getWrapper({ isMulti: true, menuIsOpen: true, value: ['filter-1', 'filter-2'] }); + const text = wrapper.render().text(); + expect(text).toContain('SelectControl.noOptions'); + }); + }); }); From df658300bf48158ca5d88113e1e7f16c26319135 Mon Sep 17 00:00:00 2001 From: Jimi Date: Fri, 10 May 2019 10:54:22 +0300 Subject: [PATCH 36/56] gitignore yarn-error.log --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index ba07412ee..42a8f1342 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ coverage dist node_modules npm-debug.log + +yarn-error\.log From 2339770346b0859b041d4420669e46e9c5e389ce Mon Sep 17 00:00:00 2001 From: Jimi Date: Fri, 10 May 2019 11:03:08 +0300 Subject: [PATCH 37/56] Remove backslash from yarn-error.log in gitignore --- .gitignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 42a8f1342..9427dbf6f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,4 @@ coverage dist node_modules npm-debug.log - -yarn-error\.log +yarn-error.log From fff302add5bf0bbaadf526dc03fae1190c250c28 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 13 May 2019 13:59:13 +0300 Subject: [PATCH 38/56] reduce amount of slot by 1 so time diff will equal minPeriod --- .../reservation-calendar/time-slots/TimeSlot.js | 4 +--- app/state/reducers/ui/reservationsReducer.js | 12 ++++++++---- app/utils/timeUtils.js | 6 ++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js index 9b095328e..230996ca5 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.js @@ -97,9 +97,7 @@ class TimeSlot extends PureComponent { onClick({ begin: slot.start, end: slot.end, - resource: resource.id, - minPeriod: resource.minPeriod === resource.slotSize ? null : resource.minPeriod - // Ignore minPeriod if minPeriod is equal slotSize + resource, }); } }; diff --git a/app/state/reducers/ui/reservationsReducer.js b/app/state/reducers/ui/reservationsReducer.js index e05466463..2e75359af 100644 --- a/app/state/reducers/ui/reservationsReducer.js +++ b/app/state/reducers/ui/reservationsReducer.js @@ -1,7 +1,6 @@ import types from 'constants/ActionTypes'; import ModalTypes from 'constants/ModalTypes'; -import omit from 'lodash/omit'; import isEmpty from 'lodash/isEmpty'; import first from 'lodash/first'; import last from 'lodash/last'; @@ -134,10 +133,15 @@ function reservationsReducer(state = initialState, action) { } case types.UI.TOGGLE_TIME_SLOT: { - const minPeriod = action.payload.minPeriod; + const { minPeriod, slotSize, id } = action.payload.resource; + let minPeriodSlot; - const startSlot = omit(action.payload, 'minPeriod'); + const startSlot = { + begin: action.payload.begin, + end: action.payload.end, + resource: id + }; // Remove minPeriod of slot information from payload. // startSlot is known as input slot from payload. @@ -145,7 +149,7 @@ function reservationsReducer(state = initialState, action) { // No time slot have been selected. // auto append minPeriodSlot to selected state to make sure minPeriod time is selected. // If minPeriod exist - minPeriodSlot = minPeriod && getEndTimeSlotWithMinPeriod(startSlot, minPeriod); + minPeriodSlot = minPeriod && getEndTimeSlotWithMinPeriod(startSlot, minPeriod, slotSize); return state.merge({ selected: minPeriod ? [startSlot, minPeriodSlot] : [startSlot] }); } diff --git a/app/utils/timeUtils.js b/app/utils/timeUtils.js index b7ecacb65..0ccd12d4f 100644 --- a/app/utils/timeUtils.js +++ b/app/utils/timeUtils.js @@ -201,8 +201,10 @@ function periodToMinute(period) { * @param {string} minPeriod * @return {object} endSlot */ -function getEndTimeSlotWithMinPeriod(startSlot, minPeriod) { - const minPeriodInMinutes = periodToMinute(minPeriod); +function getEndTimeSlotWithMinPeriod(startSlot, minPeriod, slotSize) { + const minPeriodInMinutes = periodToMinute(minPeriod) - periodToMinute(slotSize); + // minPeriod always >= slotSize + // minus 1 timeSlot here so the timediff between start slot and end slot is equal with minPeriod. return { resource: startSlot.resource, From 8e8d17b41e826e41ac48fae31c2b4d0457315ee2 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 13 May 2019 14:35:53 +0300 Subject: [PATCH 39/56] fix unit tests --- .../time-slots/TimeSlot.spec.js | 2 +- app/state/reducers/ui/reservationsReducer.js | 2 +- .../reducers/ui/reservationsReducer.spec.js | 79 ++++++++++++++----- 3 files changed, 63 insertions(+), 20 deletions(-) diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js index 48b1e7555..d02c6ec5b 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlot.spec.js @@ -242,7 +242,7 @@ describe('pages/resource/reservation-calendar/time-slots/TimeSlot', () => { { begin: defaultProps.slot.start, end: defaultProps.slot.end, - resource: defaultProps.resource.id, + resource: defaultProps.resource, }, ]); }); diff --git a/app/state/reducers/ui/reservationsReducer.js b/app/state/reducers/ui/reservationsReducer.js index 2e75359af..e9f4a25b5 100644 --- a/app/state/reducers/ui/reservationsReducer.js +++ b/app/state/reducers/ui/reservationsReducer.js @@ -163,7 +163,7 @@ function reservationsReducer(state = initialState, action) { // startSlot > minPeriodSlot => payload slot is larger than minPeriod // startSlot === minPeriodSlot => make one of them as end timeslot // startSlot < minPeriodSlot => keep minPeriod as selected. - minPeriodSlot = getEndTimeSlotWithMinPeriod(state.selected[0], minPeriod); + minPeriodSlot = getEndTimeSlotWithMinPeriod(state.selected[0], minPeriod, slotSize); const timeDiff = getTimeDiff(startSlot.begin, minPeriodSlot.begin); return state.merge({ diff --git a/app/state/reducers/ui/reservationsReducer.spec.js b/app/state/reducers/ui/reservationsReducer.spec.js index f11e6ab08..1ea1bb769 100644 --- a/app/state/reducers/ui/reservationsReducer.spec.js +++ b/app/state/reducers/ui/reservationsReducer.spec.js @@ -447,14 +447,24 @@ describe('state/reducers/ui/reservationsReducer', () => { const initialState = Immutable({ selected: [], }); - const slot = { + + const payload = { begin: '2015-10-11T10:00:00Z', end: '2015-10-11T11:00:00Z', - resource: 'some-resource', + resource: { + id: 'some-resource' + }, }; - const action = toggleTimeSlot(slot); + + const expected = Immutable([{ + begin: '2015-10-11T10:00:00Z', + end: '2015-10-11T11:00:00Z', + resource: 'some-resource' + } + + ]); + const action = toggleTimeSlot(payload); const nextState = reservationsReducer(initialState, action); - const expected = Immutable([slot]); expect(nextState.selected).toEqual(expected); expect(expected).not.toHaveProperty('minPeriod'); @@ -464,15 +474,22 @@ describe('state/reducers/ui/reservationsReducer', () => { const initialState = Immutable({ selected: [] }); - const minPeriod = '01:00:00'; - const startTimeSlot = { + const minPeriod = '02:00:00'; + + const payload = { begin: '2019-05-09T05:00:00.000Z', end: '2019-05-09T06:00:00.000Z', - resource: 'some-resource', + resource: { + id: 'some-resource', + minPeriod, + slotSize: '01:00:00' + }, }; - const payload = { - ...startTimeSlot, - minPeriod + + const startTimeSlot = { + begin: '2019-05-09T05:00:00.000Z', + end: '2019-05-09T06:00:00.000Z', + resource: 'some-resource' }; const expectedEndSlot = { @@ -499,6 +516,7 @@ describe('state/reducers/ui/reservationsReducer', () => { end: '2019-05-09T06:00:00.000Z', resource: 'some-resource', }; + const initialState = Immutable({ selected: [ originalStartTimeSlot, @@ -513,7 +531,10 @@ describe('state/reducers/ui/reservationsReducer', () => { const payload = { ...newEndTimeSlot, - minPeriod + resource: { + id: 'some-resource', + minPeriod + } }; const action = toggleTimeSlot(payload); @@ -524,6 +545,8 @@ describe('state/reducers/ui/reservationsReducer', () => { }); test('in case new end time is less than minPeriod, keep end time slot to minPeriod', () => { + const minPeriod = '03:00:00'; + const newEndTimeSlot = { begin: '2019-05-09T06:00:00.000Z', end: '2019-05-09T07:00:00.000Z', @@ -536,22 +559,27 @@ describe('state/reducers/ui/reservationsReducer', () => { end: '2019-05-09T06:00:00.000Z', resource: 'some-resource', }; + const generatedEndTimeSlot = { begin: '2019-05-09T07:00:00.000Z', end: '2019-05-09T08:00:00.000Z', resource: 'some-resource', }; + const initialState = Immutable({ selected: [ originalStartTimeSlot, generatedEndTimeSlot, ] }); - const minPeriod = '02:00:00'; const payload = { ...newEndTimeSlot, - minPeriod + resource: { + id: 'some-resource', + minPeriod, + slotSize: '01:00:00' + } }; const action = toggleTimeSlot(payload); @@ -571,7 +599,10 @@ describe('state/reducers/ui/reservationsReducer', () => { end: '2015-10-11T11:00:00Z', resource: 'some-resource', }; - const action = toggleTimeSlot(slot); + + const payload = { ...slot, resource: { id: 'some-resource' } }; + + const action = toggleTimeSlot(payload); const nextState = reservationsReducer(initialState, action); const expected = Immutable([slot]); @@ -593,7 +624,10 @@ describe('state/reducers/ui/reservationsReducer', () => { end: '2015-10-11T11:00:00ZZ', resource: 'some-resource', }; - const action = toggleTimeSlot(slot); + + const payload = { ...slot, resource: { id: 'some-resource' } }; + + const action = toggleTimeSlot(payload); const nextState = reservationsReducer(initialState, action); const expected = Immutable([...initialState.selected, slot]); @@ -623,7 +657,10 @@ describe('state/reducers/ui/reservationsReducer', () => { resource: 'some-resource', }; - const action = toggleTimeSlot(slot); + const payload = { ...slot, resource: { id: 'some-resource' } }; + + + const action = toggleTimeSlot(payload); const nextState = reservationsReducer(initialState, action); const expected = Immutable([initialState.selected[0], slot]); @@ -652,7 +689,10 @@ describe('state/reducers/ui/reservationsReducer', () => { resource: 'some-resource', }; - const action = toggleTimeSlot(slot); + const payload = { ...slot, resource: { id: 'some-resource' } }; + + + const action = toggleTimeSlot(payload); const nextState = reservationsReducer(initialState, action); const expected = Immutable([initialState.selected[0], slot]); @@ -672,7 +712,10 @@ describe('state/reducers/ui/reservationsReducer', () => { end: '2015-10-11T11:00:00Z', resource: 'some-resource', }; - const action = toggleTimeSlot(slot2); + + const payload = { ...slot2, resource: { id: 'some-resource' } }; + + const action = toggleTimeSlot(payload); const initialState = Immutable({ selected: [slot1, slot2], }); From 4713ef0484c3eb0d88cbf49363e1fd6bc789bd89 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Wed, 15 May 2019 16:33:13 +0300 Subject: [PATCH 40/56] show warning when select disabled slot by mistake --- .../ReservationCalendarContainer.js | 5 +- .../time-slots/TimeSlot.js | 52 ++++++++++++++----- .../time-slots/TimeSlots.js | 7 +-- .../resource/reservation-calendar/utils.js | 8 ++- .../reservation-calendar/utils.spec.js | 26 +++++----- 5 files changed, 59 insertions(+), 39 deletions(-) diff --git a/app/pages/resource/reservation-calendar/ReservationCalendarContainer.js b/app/pages/resource/reservation-calendar/ReservationCalendarContainer.js index 7892a40ec..2f8f37586 100644 --- a/app/pages/resource/reservation-calendar/ReservationCalendarContainer.js +++ b/app/pages/resource/reservation-calendar/ReservationCalendarContainer.js @@ -11,6 +11,7 @@ import moment from 'moment'; import first from 'lodash/first'; import last from 'lodash/last'; import orderBy from 'lodash/orderBy'; +import debounce from 'lodash/debounce'; import { addNotification } from 'actions/notificationsActions'; import { @@ -150,12 +151,12 @@ export class UnconnectedReservationCalendarContainer extends Component { const isOpen = Boolean(timeSlots.length); const showTimeSlots = isOpen && !reservingIsRestricted(resource, date); const selectedDateSlots = this.getSelectedDateSlots(timeSlots, selected); - return (
{showTimeSlots && ( { - const isUnderMinPeriod = utils.isUnderMinPeriod( - selected, timeSlot, lastSlot, resource.minPeriod - ); + const isUnderMinPeriod = utils.isUnderMinPeriod(timeSlot, lastSlot, resource.minPeriod); if (!lastSelectableFound && selected.length && timeSlot.reserved) { lastSelectableFound = utils.isSlotAfterSelected(timeSlot, selected); diff --git a/app/pages/resource/reservation-calendar/utils.js b/app/pages/resource/reservation-calendar/utils.js index 71a7b7695..6437a9819 100644 --- a/app/pages/resource/reservation-calendar/utils.js +++ b/app/pages/resource/reservation-calendar/utils.js @@ -120,22 +120,20 @@ function isHighlighted(slot, selected, hovered) { * by adding minPeriod to slot start time * and compare with last slot end time * - * @param {Object} selected * @param {Object} slot * @param {Object} lastSlot * @param {String | undefined} minPeriod: minPeriod limit, usuall HH:MM:SS * @returns */ -function isUnderMinPeriod(selected, slot, lastSlot, minPeriod) { - if (!selected || !slot || !lastSlot) { +function isUnderMinPeriod(slot, lastSlot, minPeriod) { + if (!slot || !lastSlot) { return false; } if (!slot.end || !lastSlot.end) { return false; } - - if (!selected.length && minPeriod) { + if (minPeriod) { const minPeriodInMinutes = moment.duration(minPeriod).asMinutes(); return moment(slot.start).add(minPeriodInMinutes, 'minutes') > (moment(lastSlot.end)); } diff --git a/app/pages/resource/reservation-calendar/utils.spec.js b/app/pages/resource/reservation-calendar/utils.spec.js index c013c50cf..4518e6d2b 100644 --- a/app/pages/resource/reservation-calendar/utils.spec.js +++ b/app/pages/resource/reservation-calendar/utils.spec.js @@ -194,7 +194,7 @@ describe('pages/resource/reservation-calendar/utils', () => { }); describe('isUnderMinPeriod', () => { - const minPeriod = '1:00:00'; + const minPeriod = '2:00:00'; const lastSlot = { start: '2015-10-10T15:00:00Z', end: '2015-10-10T15:30:00Z', @@ -206,26 +206,24 @@ describe('pages/resource/reservation-calendar/utils', () => { }; test('return false if required data is missing, wont throw error', () => { - const noLastSlot = utils.isUnderMinPeriod(selected, slot, undefined, minPeriod); - const noStartSlot = utils.isUnderMinPeriod(selected, undefined, lastSlot, minPeriod); - const noSelected = utils.isUnderMinPeriod(undefined, slot, lastSlot, minPeriod); + const noLastSlot = utils.isUnderMinPeriod(slot, undefined, minPeriod); + const noStartSlot = utils.isUnderMinPeriod(undefined, lastSlot, minPeriod); expect(noLastSlot).toBeFalsy(); expect(noStartSlot).toBeFalsy(); - expect(noSelected).toBeFalsy(); }); test('returns false if minPeriod is not defined', () => { - const actual = utils.isUnderMinPeriod(selected); + const actual = utils.isUnderMinPeriod(slot, lastSlot); expect(actual).toBe(false); }); test('returns false if minPeriod is defined but start time slot already selected (allow time slot to be selected if start time is already selected)', () => { - const actual = utils.isUnderMinPeriod(selected, null, null, minPeriod); + const actual = utils.isUnderMinPeriod(null, null, minPeriod); expect(actual).toBe(false); }); test('return false if its safe to select slot', () => { - const actual = utils.isUnderMinPeriod(selected, slot, lastSlot, minPeriod); + const actual = utils.isUnderMinPeriod(slot, lastSlot, minPeriod); expect(actual).toBe(false); }); @@ -234,22 +232,22 @@ describe('pages/resource/reservation-calendar/utils', () => { start: '2015-10-10T15:00:00Z', }; - const actual = utils.isUnderMinPeriod(selected, slot, closeSlot, minPeriod); + const actual = utils.isUnderMinPeriod(slot, closeSlot, minPeriod); expect(actual).toBe(false); }); test('return true if selected slot will not fulfill minPeriod', () => { - const actual = utils.isUnderMinPeriod(selected, inValidSlot, lastSlot, minPeriod); - expect(actual).toBe(false); + const actual = utils.isUnderMinPeriod(inValidSlot, lastSlot, minPeriod); + expect(actual).toBe(true); }); test('return false if selected slot fulfill minPeriod', () => { const validSlot = { - start: '2015-10-10T14:00:00Z', - end: '2015-10-10T14:30:00Z', + start: '2015-10-10T13:30:00Z', + end: '2015-10-10T14:00:00Z', }; - const actual = utils.isUnderMinPeriod(selected, validSlot, lastSlot, minPeriod); + const actual = utils.isUnderMinPeriod(validSlot, lastSlot, minPeriod); expect(actual).toBe(false); }); }); From e5b36c4a89197a13908db23e3a604a3ce1aa7c4f Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 15 May 2019 16:45:49 +0300 Subject: [PATCH 41/56] Add translation-messages for PIN-code pending --- app/i18n/messages/en.json | 1 + app/i18n/messages/fi.json | 1 + app/i18n/messages/sv.json | 1 + 3 files changed, 3 insertions(+) diff --git a/app/i18n/messages/en.json b/app/i18n/messages/en.json index a04dea7a4..b97a6ec93 100644 --- a/app/i18n/messages/en.json +++ b/app/i18n/messages/en.json @@ -147,6 +147,7 @@ "RecurringReservationControls.lastTimeLabel": "Ends on", "RecurringReservationControls.numberOfOccurrencesLabel": "Repetitions", "ReservationAccessCode.defaultText": "PIN code", + "ReservationAccessCode.pending": "PIN code is shown here. It will be generated 24h before the start of the reservation and will be sent to your email.", "ReservationCalendarPickerLegend.booked": "Booked", "ReservationCalendarPickerLegend.busy": "Partially booked", "ReservationCalendarPickerLegend.free": "Free", diff --git a/app/i18n/messages/fi.json b/app/i18n/messages/fi.json index b4e3c89f6..e71fdba94 100644 --- a/app/i18n/messages/fi.json +++ b/app/i18n/messages/fi.json @@ -147,6 +147,7 @@ "RecurringReservationControls.lastTimeLabel": "Päättyy", "RecurringReservationControls.numberOfOccurrencesLabel": "Toistokertoja", "ReservationAccessCode.defaultText": "PIN-koodi", + "ReservationAccessCode.pending": "PIN-koodi näkyy tässä. Se luodaan 24 tuntia ennen varauksen alkua ja lähetetään sähköpostiisi.", "ReservationCalendarPickerLegend.booked": "Varattu", "ReservationCalendarPickerLegend.busy": "Osittain vapaa", "ReservationCalendarPickerLegend.free": "Vapaa", diff --git a/app/i18n/messages/sv.json b/app/i18n/messages/sv.json index e2b3e070e..092d84c32 100644 --- a/app/i18n/messages/sv.json +++ b/app/i18n/messages/sv.json @@ -149,6 +149,7 @@ "RecurringReservationControls.lastTimeLabel": "Avslutas", "RecurringReservationControls.numberOfOccurrencesLabel": "Antal upprepningar", "ReservationAccessCode.defaultText": "PIN-kod", + "ReservationAccessCode.pending": "PIN-koden visas här. Den kommer att genereras 24 timmar före bokningsstart och skickas till ditt mail.", "ReservationCalendarPickerLegend.booked": "Reserverad", "ReservationCalendarPickerLegend.busy": "Delvis ledig", "ReservationCalendarPickerLegend.free": "Ledig", From 572eec3fcae9bb1fe8fabd9a09a3b0830886e2cb Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 15 May 2019 16:49:57 +0300 Subject: [PATCH 42/56] Move rename ReservationAccessCode-component for future wrapping --- .../{ReservationAccessCode.js => GeneratedAccessCode.js} | 0 ...ervationAccessCode.spec.js => GeneratedAccessCode.spec.js} | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename app/shared/reservation-access-code/{ReservationAccessCode.js => GeneratedAccessCode.js} (100%) rename app/shared/reservation-access-code/{ReservationAccessCode.spec.js => GeneratedAccessCode.spec.js} (92%) diff --git a/app/shared/reservation-access-code/ReservationAccessCode.js b/app/shared/reservation-access-code/GeneratedAccessCode.js similarity index 100% rename from app/shared/reservation-access-code/ReservationAccessCode.js rename to app/shared/reservation-access-code/GeneratedAccessCode.js diff --git a/app/shared/reservation-access-code/ReservationAccessCode.spec.js b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js similarity index 92% rename from app/shared/reservation-access-code/ReservationAccessCode.spec.js rename to app/shared/reservation-access-code/GeneratedAccessCode.spec.js index b7498d4e2..871771ff3 100644 --- a/app/shared/reservation-access-code/ReservationAccessCode.spec.js +++ b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js @@ -3,9 +3,9 @@ import Immutable from 'seamless-immutable'; import Reservation from 'utils/fixtures/Reservation'; import { shallowWithIntl } from 'utils/testUtils'; -import ReservationAccessCode from './ReservationAccessCode'; +import ReservationAccessCode from './GeneratedAccessCode'; -describe('shared/reservation-access-code/ReservationAccessCode', () => { +describe('shared/reservation-access-code/GeneratedAccessCode', () => { const defaultProps = { reservation: Immutable(Reservation.build()), }; From c2dc0815c0aab7fe0072c8696993048420adff71 Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 15 May 2019 16:54:42 +0300 Subject: [PATCH 43/56] Add component for displaying PIN-pending text --- .../reservation-access-code/PendingAccessCode.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 app/shared/reservation-access-code/PendingAccessCode.js diff --git a/app/shared/reservation-access-code/PendingAccessCode.js b/app/shared/reservation-access-code/PendingAccessCode.js new file mode 100644 index 000000000..2732e0c2f --- /dev/null +++ b/app/shared/reservation-access-code/PendingAccessCode.js @@ -0,0 +1,16 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import { injectT } from 'i18n'; + +const PendingAccessCode = ({ t }) => ( + + {t('ReservationAccessCode.pending')} + +); + +PendingAccessCode.propTypes = { + t: PropTypes.func.isRequired, +}; + +export default injectT(PendingAccessCode); From 3ea2c65c9088d0e339eea38171ff4ea005a5654c Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 15 May 2019 17:26:31 +0300 Subject: [PATCH 44/56] Replace default export with wrapper component --- .../ReservationAccessCode.js | 24 +++++++++++++++++++ app/shared/reservation-access-code/helpers.js | 11 +++++++++ 2 files changed, 35 insertions(+) create mode 100644 app/shared/reservation-access-code/ReservationAccessCode.js create mode 100644 app/shared/reservation-access-code/helpers.js diff --git a/app/shared/reservation-access-code/ReservationAccessCode.js b/app/shared/reservation-access-code/ReservationAccessCode.js new file mode 100644 index 000000000..c99e39ce4 --- /dev/null +++ b/app/shared/reservation-access-code/ReservationAccessCode.js @@ -0,0 +1,24 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +import PendingAccessCode from './PendingAccessCode'; +import GeneratedAccessCode from './GeneratedAccessCode'; +import { isAccessCodeGenerated, isAccessCodePending } from './helpers'; + +const ReservationAccessCode = ({ reservation, resource, text }) => { + if (isAccessCodeGenerated(reservation)) { + return ; + } + if (resource && isAccessCodePending(reservation, resource)) { + return ; + } + return null; +}; + +ReservationAccessCode.propTypes = { + reservation: PropTypes.object.isRequired, + resource: PropTypes.object, + text: PropTypes.string, +}; + +export default ReservationAccessCode; diff --git a/app/shared/reservation-access-code/helpers.js b/app/shared/reservation-access-code/helpers.js new file mode 100644 index 000000000..c3a053a9a --- /dev/null +++ b/app/shared/reservation-access-code/helpers.js @@ -0,0 +1,11 @@ +import has from 'lodash/has'; + +export const isAccessCodeGenerated = reservation => has(reservation, 'accessCode'); + +// eslint-disable-next-line arrow-body-style +export const isAccessCodePending = (reservation, resource) => { + // resource.generateAccessCodes: + // true => respa generates an access_code for reservation (immediately) + // false => an access_code will be generated for reservation 24h before it starts + return !isAccessCodeGenerated(reservation) && resource.generateAccessCodes === false; +}; From 3865a74784bc47c1bef57326de6f2aff68b5ed22 Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 15 May 2019 17:27:11 +0300 Subject: [PATCH 45/56] Provide resource in places where its available --- .../user-reservations/reservation-list/ReservationListItem.js | 1 + app/shared/modals/reservation-success/ReservationSuccessModal.js | 1 + 2 files changed, 2 insertions(+) diff --git a/app/pages/user-reservations/reservation-list/ReservationListItem.js b/app/pages/user-reservations/reservation-list/ReservationListItem.js index 6c4274647..d1e3cba88 100644 --- a/app/pages/user-reservations/reservation-list/ReservationListItem.js +++ b/app/pages/user-reservations/reservation-list/ReservationListItem.js @@ -52,6 +52,7 @@ class ReservationListItem extends Component {
diff --git a/app/shared/modals/reservation-success/ReservationSuccessModal.js b/app/shared/modals/reservation-success/ReservationSuccessModal.js index 170190cc6..bdfc8a51f 100644 --- a/app/shared/modals/reservation-success/ReservationSuccessModal.js +++ b/app/shared/modals/reservation-success/ReservationSuccessModal.js @@ -71,6 +71,7 @@ function ReservationSuccessModal({

From 3e9b24d775e33257b88471060ae67cf7f89e4d1c Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 15 May 2019 17:48:11 +0300 Subject: [PATCH 46/56] Changed GeneratedAccessCode to use accessCode directly as prop --- .../GeneratedAccessCode.js | 12 +++------ .../GeneratedAccessCode.spec.js | 27 +++++-------------- .../ReservationAccessCode.js | 2 +- 3 files changed, 10 insertions(+), 31 deletions(-) diff --git a/app/shared/reservation-access-code/GeneratedAccessCode.js b/app/shared/reservation-access-code/GeneratedAccessCode.js index 103aba841..d5bc55a02 100644 --- a/app/shared/reservation-access-code/GeneratedAccessCode.js +++ b/app/shared/reservation-access-code/GeneratedAccessCode.js @@ -3,24 +3,18 @@ import React from 'react'; import { injectT } from 'i18n'; -function ReservationAccessCode({ reservation, t, text }) { - if (!reservation.accessCode) { - return ; - } - +function ReservationAccessCode({ accessCode, t, text }) { return ( {text || t('ReservationAccessCode.defaultText')} {' '} - {reservation.accessCode} + {accessCode} ); } ReservationAccessCode.propTypes = { - reservation: PropTypes.shape({ - accessCode: PropTypes.string, - }).isRequired, + accessCode: PropTypes.string.isRequired, t: PropTypes.func.isRequired, text: PropTypes.string, }; diff --git a/app/shared/reservation-access-code/GeneratedAccessCode.spec.js b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js index 871771ff3..15309128b 100644 --- a/app/shared/reservation-access-code/GeneratedAccessCode.spec.js +++ b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js @@ -1,13 +1,11 @@ import React from 'react'; -import Immutable from 'seamless-immutable'; -import Reservation from 'utils/fixtures/Reservation'; import { shallowWithIntl } from 'utils/testUtils'; import ReservationAccessCode from './GeneratedAccessCode'; describe('shared/reservation-access-code/GeneratedAccessCode', () => { const defaultProps = { - reservation: Immutable(Reservation.build()), + accessCode: '1234', }; function getWrapper(extraProps) { @@ -15,39 +13,26 @@ describe('shared/reservation-access-code/GeneratedAccessCode', () => { } describe('if reservation has accessCode', () => { - const accessCode = '1234'; - const reservation = Immutable(Reservation.build({ accessCode })); - test('renders a span with correct class', () => { - const span = getWrapper({ reservation }).find('span'); + const span = getWrapper().find('span'); expect(span.length).toBe(1); expect(span.prop('className')).toBe('reservation-access-code'); }); test('renders the reservation access code', () => { - const content = getWrapper({ reservation }).text(); - expect(content).toContain(accessCode); + const content = getWrapper().text(); + expect(content).toContain('1234'); }); test('renders text given in props', () => { const text = 'Some text'; - const content = getWrapper({ reservation, text }).text(); + const content = getWrapper({ text }).text(); expect(content).toContain(text); }); test('renders default text if no text is given in props', () => { - const content = getWrapper({ reservation }).text(); + const content = getWrapper().text(); expect(content).toContain('ReservationAccessCode.defaultText'); }); }); - - describe('if reservation does not have an access code', () => { - const accessCode = null; - - test('renders an empty span', () => { - const reservation = Immutable(Reservation.build({ accessCode })); - const wrapper = getWrapper({ reservation }); - expect(wrapper.equals()).toBe(true); - }); - }); }); diff --git a/app/shared/reservation-access-code/ReservationAccessCode.js b/app/shared/reservation-access-code/ReservationAccessCode.js index c99e39ce4..bef990441 100644 --- a/app/shared/reservation-access-code/ReservationAccessCode.js +++ b/app/shared/reservation-access-code/ReservationAccessCode.js @@ -7,7 +7,7 @@ import { isAccessCodeGenerated, isAccessCodePending } from './helpers'; const ReservationAccessCode = ({ reservation, resource, text }) => { if (isAccessCodeGenerated(reservation)) { - return ; + return ; } if (resource && isAccessCodePending(reservation, resource)) { return ; From e60c5c4ae643f0297477bec1973745f2933086ac Mon Sep 17 00:00:00 2001 From: Jimi Date: Wed, 15 May 2019 17:50:40 +0300 Subject: [PATCH 47/56] Rename import in test --- .../reservation-access-code/GeneratedAccessCode.spec.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/shared/reservation-access-code/GeneratedAccessCode.spec.js b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js index 15309128b..9db268b2c 100644 --- a/app/shared/reservation-access-code/GeneratedAccessCode.spec.js +++ b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js @@ -1,7 +1,7 @@ import React from 'react'; import { shallowWithIntl } from 'utils/testUtils'; -import ReservationAccessCode from './GeneratedAccessCode'; +import GeneratedAccessCode from './GeneratedAccessCode'; describe('shared/reservation-access-code/GeneratedAccessCode', () => { const defaultProps = { @@ -9,7 +9,7 @@ describe('shared/reservation-access-code/GeneratedAccessCode', () => { }; function getWrapper(extraProps) { - return shallowWithIntl(); + return shallowWithIntl(); } describe('if reservation has accessCode', () => { From 4cecff949c0c8c4375e590a6f19becfc7721a1ff Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Thu, 16 May 2019 10:40:08 +0300 Subject: [PATCH 48/56] disable isUnderMinPeriod check anytime there are selected slot --- .../resource/reservation-calendar/time-slots/TimeSlots.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js index 2d79a4e32..ce0dc0554 100644 --- a/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js +++ b/app/pages/resource/reservation-calendar/time-slots/TimeSlots.js @@ -146,7 +146,6 @@ class TimeSlots extends Component { } const slot = timeSlots[0]; const lastSlot = timeSlots[timeSlots.length - 1]; - // last slot is selectable slot, which is slot before last slot in array const placeholderSize = timeSlotPlaceholderSizes[index]; const slotDate = moment(slot.start).format(constants.DATE_FORMAT); @@ -171,7 +170,10 @@ class TimeSlots extends Component { )} {timeSlots.map((timeSlot) => { - const isUnderMinPeriod = utils.isUnderMinPeriod(timeSlot, lastSlot, resource.minPeriod); + const isUnderMinPeriod = selected.length ? false : utils.isUnderMinPeriod( + timeSlot, lastSlot, resource.minPeriod + ); + // disable this check when time slot selected. if (!lastSelectableFound && selected.length && timeSlot.reserved) { lastSelectableFound = utils.isSlotAfterSelected(timeSlot, selected); From d444a3da9d2e10b7bba9fc09ee359293c7f285b3 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 16 May 2019 14:21:15 +0300 Subject: [PATCH 49/56] Rename stuff --- app/shared/reservation-access-code/GeneratedAccessCode.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/shared/reservation-access-code/GeneratedAccessCode.js b/app/shared/reservation-access-code/GeneratedAccessCode.js index d5bc55a02..2fc506bc9 100644 --- a/app/shared/reservation-access-code/GeneratedAccessCode.js +++ b/app/shared/reservation-access-code/GeneratedAccessCode.js @@ -3,7 +3,7 @@ import React from 'react'; import { injectT } from 'i18n'; -function ReservationAccessCode({ accessCode, t, text }) { +function GeneratedAccessCode({ accessCode, t, text }) { return ( {text || t('ReservationAccessCode.defaultText')} @@ -13,10 +13,10 @@ function ReservationAccessCode({ accessCode, t, text }) { ); } -ReservationAccessCode.propTypes = { +GeneratedAccessCode.propTypes = { accessCode: PropTypes.string.isRequired, t: PropTypes.func.isRequired, text: PropTypes.string, }; -export default injectT(ReservationAccessCode); +export default injectT(GeneratedAccessCode); From 71def46198c123a8bb51f0ec36765799cd024869 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 16 May 2019 14:21:32 +0300 Subject: [PATCH 50/56] Remove outer describe --- .../GeneratedAccessCode.spec.js | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/app/shared/reservation-access-code/GeneratedAccessCode.spec.js b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js index 9db268b2c..66998c1a1 100644 --- a/app/shared/reservation-access-code/GeneratedAccessCode.spec.js +++ b/app/shared/reservation-access-code/GeneratedAccessCode.spec.js @@ -12,27 +12,25 @@ describe('shared/reservation-access-code/GeneratedAccessCode', () => { return shallowWithIntl(); } - describe('if reservation has accessCode', () => { - test('renders a span with correct class', () => { - const span = getWrapper().find('span'); - expect(span.length).toBe(1); - expect(span.prop('className')).toBe('reservation-access-code'); - }); + test('renders a span with correct class', () => { + const span = getWrapper().find('span'); + expect(span.length).toBe(1); + expect(span.prop('className')).toBe('reservation-access-code'); + }); - test('renders the reservation access code', () => { - const content = getWrapper().text(); - expect(content).toContain('1234'); - }); + test('renders the reservation access code', () => { + const content = getWrapper().text(); + expect(content).toContain('1234'); + }); - test('renders text given in props', () => { - const text = 'Some text'; - const content = getWrapper({ text }).text(); - expect(content).toContain(text); - }); + test('renders text given in props', () => { + const text = 'Some text'; + const content = getWrapper({ text }).text(); + expect(content).toContain(text); + }); - test('renders default text if no text is given in props', () => { - const content = getWrapper().text(); - expect(content).toContain('ReservationAccessCode.defaultText'); - }); + test('renders default text if no text is given in props', () => { + const content = getWrapper().text(); + expect(content).toContain('ReservationAccessCode.defaultText'); }); }); From 5921cdc782e8798657e36fb3f6b6b754cb65ed55 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 16 May 2019 14:25:33 +0300 Subject: [PATCH 51/56] Render empty span if there is no pin and it won't be generated --- app/shared/reservation-access-code/ReservationAccessCode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/shared/reservation-access-code/ReservationAccessCode.js b/app/shared/reservation-access-code/ReservationAccessCode.js index bef990441..9a11e90a4 100644 --- a/app/shared/reservation-access-code/ReservationAccessCode.js +++ b/app/shared/reservation-access-code/ReservationAccessCode.js @@ -12,7 +12,7 @@ const ReservationAccessCode = ({ reservation, resource, text }) => { if (resource && isAccessCodePending(reservation, resource)) { return ; } - return null; + return ; }; ReservationAccessCode.propTypes = { From 7d3222c7dce7b8c6c6c37c5dd1ce2f9e17454a79 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 16 May 2019 14:26:03 +0300 Subject: [PATCH 52/56] Tests for PendingAccessCode --- .../PendingAccessCode.spec.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 app/shared/reservation-access-code/PendingAccessCode.spec.js diff --git a/app/shared/reservation-access-code/PendingAccessCode.spec.js b/app/shared/reservation-access-code/PendingAccessCode.spec.js new file mode 100644 index 000000000..ed55045ad --- /dev/null +++ b/app/shared/reservation-access-code/PendingAccessCode.spec.js @@ -0,0 +1,15 @@ +import React from 'react'; + +import { shallowWithIntl } from 'utils/testUtils'; +import PendingAccessCode from './PendingAccessCode'; + +describe('shared/reservation-access-code/PendingAccessCode', () => { + function getWrapper(extraProps) { + return shallowWithIntl(); + } + + test('renders PIN-code pending text', () => { + const wrapper = getWrapper(); + expect(wrapper.text()).toContain('ReservationAccessCode.pending'); + }); +}); From 9870b5d2ac62df374e5a05cf662df81706d25357 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 16 May 2019 14:26:27 +0300 Subject: [PATCH 53/56] Tests for ReservationAccessCode --- .../ReservationAccessCode.spec.js | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 app/shared/reservation-access-code/ReservationAccessCode.spec.js diff --git a/app/shared/reservation-access-code/ReservationAccessCode.spec.js b/app/shared/reservation-access-code/ReservationAccessCode.spec.js new file mode 100644 index 000000000..cfa49039d --- /dev/null +++ b/app/shared/reservation-access-code/ReservationAccessCode.spec.js @@ -0,0 +1,38 @@ +import React from 'react'; +import Immutable from 'seamless-immutable'; + +import Reservation from 'utils/fixtures/Reservation'; +import Resource from 'utils/fixtures/Resource'; +import { shallowWithIntl } from 'utils/testUtils'; +import ReservationAccessCode from './ReservationAccessCode'; + +describe('shared/reservation-access-code/ReservationAccessCode', () => { + const createReservation = attributes => Immutable(Reservation.build(attributes)); + const createResource = attributes => Immutable(Resource.build(attributes)); + + const defaultProps = { + reservation: createReservation(), + resource: createResource({ generateAccessCodes: true }), + }; + + function getWrapper(extraProps) { + return shallowWithIntl(); + } + + test('renders GeneratedAccessCode when PIN is available', () => { + const reservation = createReservation({ accessCode: '1232' }); + const wrapper = getWrapper({ reservation }); + expect(wrapper.name()).toBe('GeneratedAccessCode'); + }); + + test('renders PendingAccessCode when PIN is pending', () => { + const resource = createResource({ generateAccessCodes: false }); + const wrapper = getWrapper({ resource }); + expect(wrapper.name()).toBe('PendingAccessCode'); + }); + + test('renders empty span when PIN is not available and it won\'t be generated either', () => { + const wrapper = getWrapper(); + expect(wrapper.equals()).toBe(true); + }); +}); From a0bccc5b7a5e4afe63634606cd7942ca3f131357 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 16 May 2019 15:03:07 +0300 Subject: [PATCH 54/56] Interpret null accessCode as not generated --- app/shared/reservation-access-code/helpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/shared/reservation-access-code/helpers.js b/app/shared/reservation-access-code/helpers.js index c3a053a9a..3a05d414a 100644 --- a/app/shared/reservation-access-code/helpers.js +++ b/app/shared/reservation-access-code/helpers.js @@ -1,6 +1,6 @@ import has from 'lodash/has'; -export const isAccessCodeGenerated = reservation => has(reservation, 'accessCode'); +export const isAccessCodeGenerated = reservation => has(reservation, 'accessCode') && reservation.accessCode !== null; // eslint-disable-next-line arrow-body-style export const isAccessCodePending = (reservation, resource) => { From deaaa27f2e224284f9568a23caac51396a7951a3 Mon Sep 17 00:00:00 2001 From: Jimi Date: Thu, 16 May 2019 15:56:41 +0300 Subject: [PATCH 55/56] Don't render pending text if reservation is cancelled --- .../reservation-access-code/ReservationAccessCode.js | 5 ++++- .../reservation-access-code/ReservationAccessCode.spec.js | 7 +++++++ app/shared/reservation-access-code/helpers.js | 2 ++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/app/shared/reservation-access-code/ReservationAccessCode.js b/app/shared/reservation-access-code/ReservationAccessCode.js index 9a11e90a4..b6fceb53b 100644 --- a/app/shared/reservation-access-code/ReservationAccessCode.js +++ b/app/shared/reservation-access-code/ReservationAccessCode.js @@ -3,9 +3,12 @@ import PropTypes from 'prop-types'; import PendingAccessCode from './PendingAccessCode'; import GeneratedAccessCode from './GeneratedAccessCode'; -import { isAccessCodeGenerated, isAccessCodePending } from './helpers'; +import { isAccessCodeGenerated, isAccessCodePending, isReservationCancelled } from './helpers'; const ReservationAccessCode = ({ reservation, resource, text }) => { + if (isReservationCancelled(reservation)) { + return ; + } if (isAccessCodeGenerated(reservation)) { return ; } diff --git a/app/shared/reservation-access-code/ReservationAccessCode.spec.js b/app/shared/reservation-access-code/ReservationAccessCode.spec.js index cfa49039d..722a314ea 100644 --- a/app/shared/reservation-access-code/ReservationAccessCode.spec.js +++ b/app/shared/reservation-access-code/ReservationAccessCode.spec.js @@ -35,4 +35,11 @@ describe('shared/reservation-access-code/ReservationAccessCode', () => { const wrapper = getWrapper(); expect(wrapper.equals()).toBe(true); }); + + test('renders empty span when reservation that would produce pending PIN is cancelled', () => { + const reservation = createReservation({ state: 'cancelled' }); + const resource = createResource({ generateAccessCodes: false }); + const wrapper = getWrapper({ reservation, resource }); + expect(wrapper.equals()).toBe(true); + }); }); diff --git a/app/shared/reservation-access-code/helpers.js b/app/shared/reservation-access-code/helpers.js index 3a05d414a..b6839423e 100644 --- a/app/shared/reservation-access-code/helpers.js +++ b/app/shared/reservation-access-code/helpers.js @@ -9,3 +9,5 @@ export const isAccessCodePending = (reservation, resource) => { // false => an access_code will be generated for reservation 24h before it starts return !isAccessCodeGenerated(reservation) && resource.generateAccessCodes === false; }; + +export const isReservationCancelled = reservation => reservation.state === 'cancelled'; From 4c1b0608e25ab62ff1e44b162fd864f53d48aea2 Mon Sep 17 00:00:00 2001 From: Chi Nguyen Date: Mon, 20 May 2019 13:08:35 +0300 Subject: [PATCH 56/56] Prepare release 0.3.0 --- CHANGELOG.md | 123 +++++++++++++++++++++++++++++++++------------------ package.json | 2 +- 2 files changed, 82 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b7b84ff..24612572a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,82 +1,121 @@ -* 0.1.1 - ** HOTFIX ** +# 0.3.0 + **MAJOR CHANGES** + ``` + - Add translation for date-picker, show date and month in currently selected language. - - [#913](https://github.com/City-of-Helsinki/varaamo/pull/913) Fix issue staff comment section is not working. Reason: React-bootstrap ref prop which is deprecated and replace with inputProps -* 0.1.0 - ** MAJOR CHANGES ** + - Set varaamo timezone to flexible base on user local timezone. + + - Add slotSize and minPeriod to reservation select, enable ability to reserve sauna slots with default amount of minPeriod. Time slot range equal with slotSize config from backend. + + - Show access-code pending text if the access-code is generated 24h before reservation starts. + ``` + + **CHANGELOG** + + - [#920](https://github.com/City-of-Helsinki/varaamo/pull/920) Add translation for date-picker. + - [#921](https://github.com/City-of-Helsinki/varaamo/pull/921) Set varaamo timezone to flexible base on user local timezone. + - [#925](https://github.com/City-of-Helsinki/varaamo/pull/925) Add slotSize and minPeriod to reservation select, enable ability to reserve sauna with default amount of minPeriod. + - [#932](https://github.com/City-of-Helsinki/varaamo/pull/932) Show access-code pending text if the access-code is generated 24h before reservation starts. + + - [#922](https://github.com/City-of-Helsinki/varaamo/pull/922) Add municipality options to env config. + - [#924](https://github.com/City-of-Helsinki/varaamo/pull/924) Add municipality to clear filter list. + - [#927](https://github.com/City-of-Helsinki/varaamo/pull/927) Translate SelectControl noOptionsMessage. + - [#928](https://github.com/City-of-Helsinki/varaamo/pull/928), [#930](https://github.com/City-of-Helsinki/varaamo/pull/930), [#931](https://github.com/City-of-Helsinki/varaamo/pull/931) Bugfix for [#925](https://github.com/City-of-Helsinki/varaamo/pull/925) + +# 0.2.0 + **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** + + - [#913](https://github.com/City-of-Helsinki/varaamo/pull/913) Fix issue staff comment section is not working. Reason: React-bootstrap ref prop which is deprecated and replace with inputProps + +# 0.1.0 + **MAJOR CHANGES** - Replacing Karma, Chai, Mocha with Jest. - Upgrading React, Webpack, Babel and dependencies to latest. - Dockerized development environment. - Remove low locked node version 6, require v8 for Babel 7. - ** CHANGELOG ** + **CHANGELOG** + + **UI changes:** + - [#863](https://github.com/City-of-Helsinki/varaamo/pull/863): Make homepage banner clickable. - UI changes: - - #863: Make homepage banner clickable. + - [#865](https://github.com/City-of-Helsinki/varaamo/pull/865): Delayed reservation. - - #865: Delayed reservation. + - [#867](https://github.com/City-of-Helsinki/varaamo/pull/867): Add config to fetch all unit that doesn't have empty resources. - - #867: Add config to fetch all unit that doesn't have empty resources. + - [#868](https://github.com/City-of-Helsinki/varaamo/pull/868): Add municipality filters for filtering resources base on municiples. - - #868: Add municipality filters for filtering resources base on municiples. + - [#873](https://github.com/City-of-Helsinki/varaamo/pull/873): Limit the selection of time slots to the ones within max period. - - #873: Limit the selection of time slots to the ones within max period. + - [#875](https://github.com/City-of-Helsinki/varaamo/pull/875): Expand advanced search panel when filters are applied. - - #875: Expand advanced search panel when filters are applied. + - [#876](https://github.com/City-of-Helsinki/varaamo/pull/876): Free-of-charge filter for resources. - - #876: Free-of-charge filter for resources. + - [#878](https://github.com/City-of-Helsinki/varaamo/pull/878): Remove the link for old website from the footer. - - #878: Remove the link for old website from the footer. + - [#883](https://github.com/City-of-Helsinki/varaamo/pull/883): Clear all filters after reset. - - #883: Clear all filters after reset. + - [#889](https://github.com/City-of-Helsinki/varaamo/pull/889): Disable reservation time limit for admins. - - #889: Disable reservation time limit for admins. + - [#899](https://github.com/City-of-Helsinki/varaamo/pull/899): Add unpublished tag to resource search list. - - #899: Add unpublished tag to resource search list. + - [#901](https://github.com/City-of-Helsinki/varaamo/pull/901): Add navigation links to staff and higher permission user. - - #901: Add navigation links to staff and higher permission user. + **Upgrading:** - 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 + - 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. + - Remove redux-localstorage and redux-localstorage-filter. Replaced with vanilla code. - - Upgrade redux and dependencies. + - 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. + - 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. + - Delete navbar, sidebar, side-navbar which was replaced by new component but not getting removed. diff --git a/package.json b/package.json index ab74cc017..caf92c7cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "varaamo", - "version": "0.1.1", + "version": "0.3.0", "repository": { "type": "git", "url": "https://github.com/City-of-Helsinki/varaamo"