From a0cafd1906255bcced223fb5f92bd43df552bd46 Mon Sep 17 00:00:00 2001 From: Pauliina Ilmanen Date: Tue, 5 Nov 2024 15:25:01 +0200 Subject: [PATCH 1/2] Add service filter to GeographicalUnitList view Add possibility to filter services in GeographicalUnitList view in same way as in the StatisticalDistrictUnitList view. --- src/i18n/en.js | 1 + src/i18n/fi.js | 1 + src/i18n/sv.js | 1 + .../GeographicalUnitList.js | 42 ++++++- .../ServiceFilterContainer.js | 85 ++++++++++++++ .../ServiceFilterContainer/index.js | 3 + .../StatisticalDistrictUnitListComponent.js | 110 ++---------------- .../AreaView/components/styled/styled.js | 41 ++++++- src/views/AreaView/serviceFilterStyles.js | 13 +++ 9 files changed, 193 insertions(+), 104 deletions(-) create mode 100644 src/views/AreaView/components/ServiceFilterContainer/ServiceFilterContainer.js create mode 100644 src/views/AreaView/components/ServiceFilterContainer/index.js create mode 100644 src/views/AreaView/serviceFilterStyles.js diff --git a/src/i18n/en.js b/src/i18n/en.js index b3f5218a3..7d96737b7 100644 --- a/src/i18n/en.js +++ b/src/i18n/en.js @@ -74,6 +74,7 @@ export default { 'area.neighborhood.title': 'Choose neighbourhood', 'area.postcode_area.title': 'Choose postal code', 'area.major_district.title': 'Choose major district', + 'area.service.filter': 'Filtering of services for areas', 'area.statisticalDistrict.info': 'First, select a population data area, and then you can browse the area\'s services', 'area.statisticalDistrict.title': 'Select a population data area', 'area.statisticalDistrict.section': 'Cropping: {text}', diff --git a/src/i18n/fi.js b/src/i18n/fi.js index dc7f9ba66..c7a73f670 100644 --- a/src/i18n/fi.js +++ b/src/i18n/fi.js @@ -74,6 +74,7 @@ export default { 'area.neighborhood.title': 'Valitse kaupunginosa', 'area.postcode_area.title': 'Valitse postinumero', 'area.major_district.title': 'Valitse suurpiiri', + 'area.service.filter': 'Palvelujen suodatus', 'area.statisticalDistrict.info': 'Valitse ensin väestötietoalue, jonka jälkeen voit selata alueen palveluita', 'area.statisticalDistrict.label': '{count} henkilöä, {percent}% alueen koko väestöstä', 'area.statisticalDistrict.label.total': '{count} henkilöä', diff --git a/src/i18n/sv.js b/src/i18n/sv.js index 8507bd36a..ab5979e52 100644 --- a/src/i18n/sv.js +++ b/src/i18n/sv.js @@ -74,6 +74,7 @@ export default { 'area.neighborhood.title': 'Välj stadsdel', 'area.postcode_area.title': 'Välj postnummer', 'area.major_district.title': 'Välj stordistrikt', + 'area.service.filter': 'Filtrering av geografiska tjänster', 'area.statisticalDistrict.info': 'Välj först befolkningsdataområdet, varefter du kan bläddra bland regionens tjänster', 'area.statisticalDistrict.title': 'Välj befolkningsdataområde', 'area.statisticalDistrict.section': 'Beskärning: {text}', diff --git a/src/views/AreaView/components/GeographicalUnitList/GeographicalUnitList.js b/src/views/AreaView/components/GeographicalUnitList/GeographicalUnitList.js index 98afeefa6..2571c8008 100644 --- a/src/views/AreaView/components/GeographicalUnitList/GeographicalUnitList.js +++ b/src/views/AreaView/components/GeographicalUnitList/GeographicalUnitList.js @@ -1,7 +1,10 @@ import { Checkbox, List, Typography } from '@mui/material'; import PropTypes from 'prop-types'; -import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import React, { + useCallback, useEffect, useMemo, useRef, useState, +} from 'react'; import { useDispatch, useSelector } from 'react-redux'; +import { useIntl } from 'react-intl'; import { UnitItem } from '../../../../components'; import { addSelectedDistrictService, @@ -13,7 +16,7 @@ import { selectSelectedDistrictServices, } from '../../../../redux/selectors/district'; import { getLocale } from '../../../../redux/selectors/user'; -import { uppercaseFirst } from '../../../../utils'; +import { keyboardHandler, uppercaseFirst } from '../../../../utils'; import { orderUnits } from '../../../../utils/orderUnits'; import useLocaleText from '../../../../utils/useLocaleText'; import { @@ -24,6 +27,7 @@ import { StyledUnitList, StyledUnitListArea, } from '../styled/styled'; +import ServiceFilterContainer from '../ServiceFilterContainer/ServiceFilterContainer'; // Custom uncontrolled checkbox that allows default value const UnitCheckbox = ({ @@ -56,7 +60,16 @@ const GeographicalUnitList = ({ initialOpenItems }) => { const locale = useSelector(getLocale); const [serviceList, setServiceList] = useState([]); const [initialCheckedItems] = useState(selectedServices); - + const inputRef = useRef(); + const { formatMessage } = useIntl(); + const [filterValue, setFilterValue] = useState(''); + const title = formatMessage({ id: 'area.service.filter' }); + + const handlefilterButtonClick = () => { + if (inputRef) { + setFilterValue(inputRef.current.value); + } + }; const handleUnitCheckboxChange = useCallback((event, id) => { if (event.target.checked) { @@ -112,18 +125,37 @@ const GeographicalUnitList = ({ initialOpenItems }) => { if (emptyCategories.length) { dispatch(removeSelectedDistrictService(emptyCategories)); } + + // Use filter + if (filterValue) { + const filteredServiceList = serviceList.filter((category) => { + const name = getLocaleText(category.name); + return name.toLowerCase().includes(filterValue.toLowerCase()); + }); + setServiceList(filteredServiceList); + return; + } + setServiceList(serviceList); }; useEffect(() => { createServiceCategories(); - }, [filteredSubdistrictUnits]); - + }, [filteredSubdistrictUnits, filterValue]); // Render list of units for neighborhood and postcode-area subdistricts const renderUnitList = useMemo(() => ( + {serviceList.map(category => ( { + const theme = useTheme(); + const { + serviceFilterInputClass, + serviceFilterButtonLabelClass, + serviceFilterButtonFocusClass, + } = createServiceFilterStyles(theme); + + return ( + + {typeof title === 'string' && ( + + {title} + + )} + + handlefilterButtonClick(), ['enter'])} + endAdornment={ + filterValue ? ( + { + inputRef.current.value = ''; + setFilterValue(''); + }} + > + + + ) : null + } + /> + + + + + + + + ); +}; + +export default ServiceFilterContainer; diff --git a/src/views/AreaView/components/ServiceFilterContainer/index.js b/src/views/AreaView/components/ServiceFilterContainer/index.js new file mode 100644 index 000000000..0fb8aead0 --- /dev/null +++ b/src/views/AreaView/components/ServiceFilterContainer/index.js @@ -0,0 +1,3 @@ +import ServiceFilterContainer from './ServiceFilterContainer'; + +export default ServiceFilterContainer; diff --git a/src/views/AreaView/components/StatisticalDistrictUnitList/StatisticalDistrictUnitListComponent.js b/src/views/AreaView/components/StatisticalDistrictUnitList/StatisticalDistrictUnitListComponent.js index 47de90329..edd985313 100644 --- a/src/views/AreaView/components/StatisticalDistrictUnitList/StatisticalDistrictUnitListComponent.js +++ b/src/views/AreaView/components/StatisticalDistrictUnitList/StatisticalDistrictUnitListComponent.js @@ -1,14 +1,10 @@ -import { css } from '@emotion/css'; -import { Clear } from '@mui/icons-material'; import { - Button, Checkbox, IconButton, InputBase, List, Typography, + Checkbox, List, Typography, } from '@mui/material'; -import { styled } from '@mui/material/styles'; -import { useTheme } from '@mui/styles'; import { visuallyHidden } from '@mui/utils'; import PropTypes from 'prop-types'; import React, { useMemo, useRef, useState } from 'react'; -import { FormattedMessage, useIntl } from 'react-intl'; +import { useIntl } from 'react-intl'; import { useSelector } from 'react-redux'; import { UnitItem } from '../../../../components'; import { @@ -22,6 +18,7 @@ import { StyledUnitList, StyledUnitListArea, } from '../styled/styled'; +import ServiceFilterContainer from '../ServiceFilterContainer/ServiceFilterContainer'; // Custom uncontrolled checkbox that allows default value const UnitCheckbox = ({ @@ -53,7 +50,6 @@ const StatisticalDistrictUnitListComponent = ({ const inputRef = useRef(); const { formatMessage } = useIntl(); const getLocaleText = useLocaleText(); - const theme = useTheme(); const statisticalDistrictUnits = useSelector(getServiceFilteredStatisticalDistrictUnits); const [initialCheckedItems] = useState(selectedServices || []); const [filterValue, setFilterValue] = useState(''); @@ -69,67 +65,19 @@ const StatisticalDistrictUnitListComponent = ({ setFilterValue(inputRef.current.value); } }; - const serviceFilterInputClass = css({ - margin: theme.spacing(1), - }); - const serviceFilterButtonLabelClass = css({ - flexDirection: 'column', - }); - const serviceFilterButtonFocusClass = css({ - boxShadow: `0 0 0 4px ${theme.palette.focusBorder.main} !important`, - }); // Render list of units for neighborhood and postcode-area subdistricts const renderServiceList = useMemo(() => ( - - { - typeof title === 'string' && ( - {title} - ) - } - - handlefilterButtonClick(), ['enter'])} - endAdornment={ - filterValue - ? ( - { - inputRef.current.value = ''; - setFilterValue(''); - }} - > - - - ) - : null - } - /> - - - - - + { filterValue && filterValue !== '' @@ -204,37 +152,3 @@ UnitCheckbox.defaultProps = { }; export default StatisticalDistrictUnitListComponent; - -const StyledRowContainer = styled('div')` - display: flex; - flex-direction: row; - justify-content: space-between; -`; -const StyledServiceFilterContainer = styled('div')(({ theme }) => ({ - padding: theme.spacing(2), - paddingLeft: 72, - display: 'flex', - flexDirection: 'column', -})); -const StyledServiceFilterText = styled(Typography)(({ theme }) => ({ - paddingBottom: theme.spacing(1), - fontWeight: 'bold', -})); -const StyledServiceFilter = styled(InputBase)(({ theme }) => ({ - backgroundColor: theme.palette.white.main, - flex: '1 0 auto', -})); -const StyledServiceFilterButton = styled(Button)(({ theme }) => ({ - flex: '0 0 auto', - borderRadius: 0, - borderTopRightRadius: 4, - borderBottomRightRadius: 4, - boxShadow: 'none', - padding: theme.spacing(1, 2), - textTransform: 'none', - '& svg': { - fontSize: 20, - marginBottom: theme.spacing(0.5), - }, - flexDirection: 'column', -})); diff --git a/src/views/AreaView/components/styled/styled.js b/src/views/AreaView/components/styled/styled.js index 3994cbcd4..f8bb763e2 100644 --- a/src/views/AreaView/components/styled/styled.js +++ b/src/views/AreaView/components/styled/styled.js @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { - Divider, List, ListItem, Typography, + Divider, List, ListItem, Typography, InputBase, Button, } from '@mui/material'; import { SMAccordion } from '../../../../components'; @@ -113,6 +113,40 @@ const StyledUnitList = styled(List)(({ theme }) => ({ paddingBottom: theme.spacing(1), })); +const StyledRowContainer = styled('div')` + display: flex; + flex-direction: row; + justify-content: space-between; +`; +const StyledServiceFilterContainer = styled('div')(({ theme }) => ({ + padding: theme.spacing(2), + paddingLeft: 72, + display: 'flex', + flexDirection: 'column', +})); +const StyledServiceFilterText = styled(Typography)(({ theme }) => ({ + paddingBottom: theme.spacing(1), + fontWeight: 'bold', +})); +const StyledServiceFilter = styled(InputBase)(({ theme }) => ({ + backgroundColor: theme.palette.white.main, + flex: '1 0 auto', +})); +const StyledServiceFilterButton = styled(Button)(({ theme }) => ({ + flex: '0 0 auto', + borderRadius: 0, + borderTopRightRadius: 4, + borderBottomRightRadius: 4, + boxShadow: 'none', + padding: theme.spacing(1, 2), + textTransform: 'none', + '& svg': { + fontSize: 20, + marginBottom: theme.spacing(0.5), + }, + flexDirection: 'column', +})); + export { StyledDivider, StyledDistrictServiceList, @@ -134,4 +168,9 @@ export { StyledUnitListArea, StyledAccordionServiceTitle, StyledUnitList, + StyledRowContainer, + StyledServiceFilterContainer, + StyledServiceFilterText, + StyledServiceFilter, + StyledServiceFilterButton, }; diff --git a/src/views/AreaView/serviceFilterStyles.js b/src/views/AreaView/serviceFilterStyles.js new file mode 100644 index 000000000..32599b038 --- /dev/null +++ b/src/views/AreaView/serviceFilterStyles.js @@ -0,0 +1,13 @@ +import { css } from '@emotion/css'; + +export const createServiceFilterStyles = (theme) => ({ + serviceFilterInputClass: css({ + margin: theme.spacing(1), + }), + serviceFilterButtonLabelClass: css({ + flexDirection: 'column', + }), + serviceFilterButtonFocusClass: css({ + boxShadow: `0 0 0 4px ${theme.palette.focusBorder.main} !important`, + }), +}); From 2f3418c2fd4b4fb9882ae8a45508820d9fbb9214 Mon Sep 17 00:00:00 2001 From: Pauliina Ilmanen Date: Mon, 18 Nov 2024 14:07:49 +0200 Subject: [PATCH 2/2] Replace old reservations API with new one Fetch reservations from the new reservations API. --- .env.example | 6 ++- .github/workflows/ci.yml | 2 +- server/dataFetcher.js | 2 +- .../ReservationItem/ReservationItem.js | 33 +++++++++++--- src/redux/actions/selectedUnitReservations.js | 43 ++++++------------- src/utils/fetch/constants.js | 8 ++-- .../components/ExtendedData/ExtendedData.js | 4 +- .../components/UnitDataList/UnitDataList.js | 4 +- 8 files changed, 53 insertions(+), 49 deletions(-) diff --git a/.env.example b/.env.example index 1de37a7a9..c2af37afb 100644 --- a/.env.example +++ b/.env.example @@ -9,7 +9,11 @@ ACCESSIBILITY_SENTENCE_API="https://www.hel.fi/palvelukarttaws/rest/v4" SERVICEMAP_API="https://api.hel.fi/servicemap/" SERVICEMAP_API_VERSION="v2" EVENTS_API="https://api.hel.fi/linkedevents/v1" -RESERVATIONS_API="https://api.hel.fi/respa/v1" + +RESERVATIONS_API="https://tilavaraus.hel.fi" # For production +# RESERVATIONS_API="https://tilavaraus.test.hel.ninja" # For test environment +# RESERVATIONS_API="https://tilavaraus.dev.hel.ninja" # For local development + FEEDBACK_URL="https://api.hel.fi/servicemap/open311/" DIGITRANSIT_API="https://api.digitransit.fi/routing/v1/routers/hsl/index/graphql" # This is key for DIGITRANSIT API. There are separate urls and keys for prod (api.digitransit.fi) and dev (dev-api.digitransit.fi) digitransit apis. Developer should generate own diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4cfc426a..1ee1da059 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,7 +21,7 @@ jobs: SERVICEMAP_API: https://api.hel.fi/servicemap/ SERVICEMAP_API_VERSION: "v2" EVENTS_API: https://api.hel.fi/linkedevents/v1 - RESERVATIONS_API: https://api.hel.fi/respa/v1 + RESERVATIONS_API: https://tilavaraus.hel.fi PRODUCTION_PREFIX: SM DIGITRANSIT_API: https://api.digitransit.fi/routing/v1/routers/hsl/index/graphql DIGITRANSIT_API_KEY: a9219bfb875d4cc79b5f69123b57d0db diff --git a/server/dataFetcher.js b/server/dataFetcher.js index fd7e47f85..ab817fd01 100644 --- a/server/dataFetcher.js +++ b/server/dataFetcher.js @@ -180,7 +180,7 @@ export const fetchSelectedUnitData = (req, res, next) => { store.dispatch(fetchSuccess(data.results)); response(); } - reservationsFetch({ unit: `tprek:${id}` }, null, reservationFetchEnd, fetchOnError, null, null, controller) + reservationsFetch(null, null, reservationFetchEnd, fetchOnError, null, id, controller); } catch(e) { console.log('Error in fetchSelectedUnitData', e.message); diff --git a/src/components/ListItems/ReservationItem/ReservationItem.js b/src/components/ListItems/ReservationItem/ReservationItem.js index e688ac60a..3fdc53838 100644 --- a/src/components/ListItems/ReservationItem/ReservationItem.js +++ b/src/components/ListItems/ReservationItem/ReservationItem.js @@ -2,20 +2,37 @@ import React from 'react'; import PropTypes from 'prop-types'; import { EventAvailable } from '@mui/icons-material'; import SimpleListItem from '../SimpleListItem'; -import useLocaleText from '../../../utils/useLocaleText'; +import config from '../../../../config'; +import { useSelector } from 'react-redux'; +import { getLocale } from '../../../redux/selectors/user'; + +const getLocalizedText = (reservation, locale) => { + switch (locale) { + case 'fi': + return reservation.name_fi; + case 'en': + return reservation.name_en; + case 'sv': + return reservation.name_sv; + default: + return reservation.name_fi; // Fallback to Finnish + } +}; const ReservationItem = ({ reservation, intl, divider }) => { - const getLocaleText = useLocaleText(); + const locale = useSelector(getLocale); + const localizedText = getLocalizedText(reservation, locale); + return ( } link - text={`${getLocaleText(reservation.name)} ${intl.formatMessage({ id: 'opens.new.tab' })}`} + text={`${localizedText} ${intl.formatMessage({ id: 'opens.new.tab' })}`} divider={divider} handleItemClick={() => { - window.open(`https://varaamo.hel.fi/resources/${reservation.id}`); + window.open(`${config.reservationsAPI.root}/${locale}/reservation-unit/${reservation.pk}`); }} /> ); @@ -24,8 +41,10 @@ const ReservationItem = ({ reservation, intl, divider }) => { ReservationItem.propTypes = { intl: PropTypes.objectOf(PropTypes.any).isRequired, reservation: PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.objectOf(PropTypes.any), + pk: PropTypes.number, + name_fi: PropTypes.string, + name_en: PropTypes.string, + name_sv: PropTypes.string, }).isRequired, divider: PropTypes.bool, }; diff --git a/src/redux/actions/selectedUnitReservations.js b/src/redux/actions/selectedUnitReservations.js index 35e97ae86..a45067814 100644 --- a/src/redux/actions/selectedUnitReservations.js +++ b/src/redux/actions/selectedUnitReservations.js @@ -2,48 +2,31 @@ import { reservations } from './fetchDataActions'; import { reservationsFetch } from '../../utils/fetch'; const { - isFetching, fetchSuccess, fetchMoreSuccess, fetchError, fetchProgressUpdate, + isFetching, fetchSuccess, fetchError } = reservations; -export const fetchReservations = (id, pageSize, all = false) => async (dispatch, getState) => { - const { selectedUnit } = getState(); - const { reservations } = selectedUnit; - const previousFetch = reservations.previousSearch; - if (previousFetch) { - const parts = previousFetch.split('-'); - if (parts[0] === id && parts[1] === 'all') { - return; - } - } +export const fetchReservations = (id) => async (dispatch, getState) => { + const onStart = () => { - dispatch(isFetching(`${id}-${all ? 'all' : 'partial'}`)); + dispatch(isFetching(id)); }; + const onSuccess = (data) => { if (data && data.length) { dispatch(fetchSuccess(data)); return; } - dispatch(fetchProgressUpdate(data.results.length, data.count, data.next)); dispatch(fetchSuccess(data.results, { count: data.count, next: data.next })); }; - const onError = e => dispatch(fetchError(e.message)); - const onNext = all ? (resultTotal, response) => { - dispatch(fetchProgressUpdate(resultTotal.length, response.count)); - } : null; - // Fetch data - reservationsFetch({ unit: `tprek:${id}`, page_size: pageSize || 5 }, onStart, onSuccess, onError, onNext); -}; - - -export const fetchAdditionalReservations = next => async (dispatch) => { - // fetch additional data that is added to previous data - const onStart = () => dispatch(isFetching()); - const onSuccess = (data) => { - dispatch(fetchMoreSuccess(data.results, { count: data.count, next: data.next })); + const onError = e => { + console.error(e); // Log the error + dispatch(fetchError(e.message)); }; - const onError = e => dispatch(fetchError(e.message)); - // Fetch data - reservationsFetch(null, onStart, onSuccess, onError, null, null, null, next); + try { + await reservationsFetch(null, onStart, onSuccess, onError, null, id, null, null, null); + } catch (e) { + onError(e); + } }; diff --git a/src/utils/fetch/constants.js b/src/utils/fetch/constants.js index c8b993569..907ef6a86 100644 --- a/src/utils/fetch/constants.js +++ b/src/utils/fetch/constants.js @@ -26,11 +26,9 @@ export const APIHandlers = { envName: config.serviceMapAPI.id, }, reservations: { - url: `${config.reservationsAPI.root}/resource/`, - options: { - page_size: 5, - }, - envName: config.serviceMapAPI.id, + url: id => `${config.reservationsAPI.root}/v1/palvelukartta/reservation-units/${id}`, + options: {}, + envName: config.reservationsAPI.id, }, search: { url: `${config.serviceMapAPI.root}${config.serviceMapAPI.version}/search/`, diff --git a/src/views/UnitView/components/ExtendedData/ExtendedData.js b/src/views/UnitView/components/ExtendedData/ExtendedData.js index a241b707f..a0c637f09 100644 --- a/src/views/UnitView/components/ExtendedData/ExtendedData.js +++ b/src/views/UnitView/components/ExtendedData/ExtendedData.js @@ -38,7 +38,7 @@ const ExtendedData = ({ fetchUnitEvents(unit, 50, true); break; case 'reservations': - fetchReservations(unit, 20, true); + fetchReservations(unit, true); break; default: } @@ -163,7 +163,7 @@ const ExtendedData = ({ id="reservations" data={data || []} customComponent={item => ( - + )} srTitle={srTitle} title={titleText} diff --git a/src/views/UnitView/components/UnitDataList/UnitDataList.js b/src/views/UnitView/components/UnitDataList/UnitDataList.js index dd67a7819..70cf4f6cb 100644 --- a/src/views/UnitView/components/UnitDataList/UnitDataList.js +++ b/src/views/UnitView/components/UnitDataList/UnitDataList.js @@ -22,7 +22,7 @@ const UnitDataList = ({ const dataItems = data.data; let fullDataLength; - if (type === 'educationServices') { + if (type === 'educationServices' || type === 'reservations') { fullDataLength = dataItems?.length; } else { fullDataLength = data.max; @@ -57,7 +57,7 @@ const UnitDataList = ({ } if (type === 'reservations') { return ( shownData.map((item, i) => ( - + )) ); }