digitransit-ui/app/component/WithSearchContext.js
Vesa Meskanen a2e07e2ff3 chore: tune old search cleanup
Label can change when searching with or without alternate name
e.g. search steissi gives diffrent labe for same item.
Therefore compare name, street and address directly.
Code rearranged due to strange javascript execution problems.
2023-07-03 15:37:13 +03:00

308 lines
9.5 KiB
JavaScript

import React from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'react-intl';
import getJson from '@digitransit-search-util/digitransit-search-util-get-json';
import suggestionToLocation from '@digitransit-search-util/digitransit-search-util-suggestion-to-location';
import connectToStores from 'fluxible-addons-react/connectToStores';
import { addAnalyticsEvent } from '../util/analyticsUtils';
import {
PREFIX_ITINERARY_SUMMARY,
PREFIX_STOPS,
PREFIX_ROUTES,
} from '../util/path';
import searchContext from '../util/searchContext';
import intializeSearchContext from '../util/DTSearchContextInitializer';
import SelectFromMapHeader from './SelectFromMapHeader';
import SelectFromMap from './map/SelectFromMap';
import DTModal from './DTModal';
import FromMapModal from './FromMapModal';
import { removeSearch } from '../action/SearchActions';
const PATH_OPTS = {
stopsPrefix: PREFIX_STOPS,
routesPrefix: PREFIX_ROUTES,
itinerarySummaryPrefix: PREFIX_ITINERARY_SUMMARY,
};
export default function withSearchContext(
WrappedComponent,
embeddedSearch = false,
) {
class ComponentWithSearchContext extends React.Component {
static contextTypes = {
config: PropTypes.object.isRequired,
intl: intlShape.isRequired,
executeAction: PropTypes.func.isRequired,
getStore: PropTypes.func.isRequired,
};
static propTypes = {
selectHandler: PropTypes.func.isRequired,
locationState: PropTypes.object.isRequired,
onGeolocationStart: PropTypes.func,
fromMap: PropTypes.string,
isMobile: PropTypes.bool,
favouriteContext: PropTypes.bool,
showViapointControl: PropTypes.bool,
};
static defaultProps = {
onGeolocationStart: null,
fromMap: undefined,
isMobile: false,
showViapointControl: false,
};
constructor(props) {
super(props);
this.state = {
// eslint-disable-next-line react/no-unused-state
pendingCurrentLocation: false,
isInitialized: false,
positioningSelectedFrom: '',
fromMap: this.props.fromMap,
};
}
static getDerivedStateFromProps(nextProps, prevState) {
if (prevState.isInitialized) {
const locState = nextProps.locationState;
if (
(prevState.pendingCurrentLocation &&
locState.status === 'found-address') ||
locState.locationingFailed
) {
return {
pendingCurrentLocation: false,
positioningSelectedFrom: null,
};
}
}
return null;
}
componentDidMount() {
if (!this.state.isInitialized) {
intializeSearchContext(this.context, searchContext);
this.setState({ isInitialized: true });
}
}
componentDidUpdate(prevProps, prevState) {
if (
prevState.pendingCurrentLocation !== this.state.pendingCurrentLocation
) {
const locState = this.props.locationState;
if (locState.status === 'found-address') {
this.onSuggestionSelected(
locState,
prevState.positioningSelectedFrom,
);
}
}
}
storeReference = ref => {
this.setState(prevState => ({ refs: [...prevState.refs, ref] }));
};
saveOldSearch = (item, type, id) => {
if (
item.type !== 'FutureRoute' &&
item.type !== 'CurrentLocation' &&
item.type !== 'SelectFromMap' &&
item.type.indexOf('Favourite') === -1 &&
id !== 'favourite' &&
(!item.properties ||
!item.properties.layer ||
item.properties.layer.indexOf('favourite') === -1)
) {
this.context.executeAction(searchContext.saveSearch, {
item,
type,
});
}
};
onSuggestionSelected = (item, id) => {
if (item.type === 'SelectFromMap') {
this.setState({ fromMap: id });
} else if (id !== 'stop-route-station' && item.type !== 'FutureRoute') {
let location;
if (item.type === 'CurrentLocation') {
if (embeddedSearch) {
this.props.selectHandler(
{
type: 'CurrentLocation',
status: 'no-location',
address: this.context.intl.formatMessage({
id: 'own-position',
defaultMessage: 'Own Location',
}),
},
id,
);
return;
}
// item is already a location.
location = item;
if (
item.properties &&
item.properties.layer === 'currentPosition' &&
!item.properties.lat
) {
// eslint-disable-next-line react/no-unused-state
this.setState(
{ pendingCurrentLocation: true, positioningSelectedFrom: id },
this.context.executeAction(searchContext.startLocationWatch),
);
if (this.props.onGeolocationStart) {
this.props.onGeolocationStart(item, id);
}
return;
}
if (!location.address) {
location.address = this.context.intl.formatMessage({
id: 'own-position',
defaultMessage: 'Own Location',
});
}
} else {
location = suggestionToLocation(item);
}
this.props.selectHandler(location, id);
} else {
this.props.selectHandler(item, id);
}
};
// top level onSelect callback manages search history
onSelect = (item, id) => {
// type for storing old searches. 'endpoint' types are available in itinerary search
let type = 'endpoint';
switch (item.type) {
case 'Route':
type = 'search'; // can't be used as location
break;
default:
}
if (item.type === 'OldSearch' && item.properties.gid) {
getJson(this.context.config.URL.PELIAS_PLACE, {
ids: item.properties.gid,
})
.then(res => {
const newItem = { ...item };
let canSave = true;
if (res.features != null && res.features.length > 0) {
// update only position. It is surprising if, say, the name changes at selection.
const geom = res.features[0].geometry;
newItem.geometry.coordinates = geom.coordinates;
if (
newItem.properties.name !== res.features[0].properties.name ||
newItem.properties.street !==
res.features[0].properties.street ||
newItem.properties.housenumber !==
res.features[0].properties.housenumber
) {
// Item properties have changed unexpectedly. For example,
// an enterprise may have moved to new premises. Remove outdated information.
canSave = false;
}
}
if (canSave) {
this.saveOldSearch(newItem, type, id);
} else {
this.context.executeAction(removeSearch, {
item: newItem,
type,
});
}
this.onSuggestionSelected(item, id);
})
.catch(() => {
this.saveOldSearch(item, type, id);
this.onSuggestionSelected(item, id);
});
} else {
this.saveOldSearch(item, type, id);
this.onSuggestionSelected(item, id);
}
};
confirmMapSelection = (type, mapLocation) => {
this.setState({ fromMap: undefined }, () =>
this.props.selectHandler(mapLocation, type),
);
};
renderSelectFromMapModal = id => {
let titleId = 'select-from-map-no-title';
if (id === 'origin') {
titleId = 'select-from-map-origin';
} else if (id === 'destination') {
titleId = 'select-from-map-destination';
} else if (id === 'favourite') {
titleId = 'select-from-map-favourite';
} else if (id === parseInt(id, 10)) {
// id = via point index
titleId = 'select-from-map-viaPoint';
}
if (!this.props.isMobile) {
return (
<FromMapModal
onClose={() => this.setState({ fromMap: undefined })}
titleId={titleId}
favouriteContext={this.props.favouriteContext}
>
<SelectFromMap type={id} onConfirm={this.confirmMapSelection} />
</FromMapModal>
);
}
return (
<DTModal show>
<SelectFromMapHeader
titleId={titleId}
onBackBtnClick={() => this.setState({ fromMap: undefined })}
hideCloseBtn
/>
<SelectFromMap type={id} onConfirm={this.confirmMapSelection} />
</DTModal>
);
};
render() {
const { fromMap } = this.state;
if (fromMap !== undefined) {
return this.renderSelectFromMapModal(fromMap);
}
const viaProps = this.props.showViapointControl
? { handleViaPointLocationSelected: this.onSelect }
: {};
return (
<WrappedComponent
appElement="#app"
searchContext={searchContext}
addAnalyticsEvent={addAnalyticsEvent}
onSelect={this.onSelect}
{...this.props}
{...viaProps}
pathOpts={PATH_OPTS}
/>
);
}
}
const componentWithPosition = connectToStores(
ComponentWithSearchContext,
['PositionStore'],
context => ({
locationState: context.getStore('PositionStore').getLocationState(),
}),
);
return componentWithPosition;
}