diff --git a/app/component/IndexPage.js b/app/component/IndexPage.js
index c607bebf9a..3c24d40197 100644
--- a/app/component/IndexPage.js
+++ b/app/component/IndexPage.js
@@ -300,20 +300,30 @@ class IndexPage extends React.Component {
const destination = this.pendingDestination || this.props.destination;
const sources = ['Favourite', 'History', 'Datasource'];
const stopAndRouteSearchTargets = ['Stops', 'Routes'];
- const locationSearchTargets = [
+ let locationSearchTargets = [
'Locations',
'CurrentPosition',
'FutureRoutes',
- 'Stops',
];
- if (useCitybikes(config.vehicleRental?.networks, config)) {
- stopAndRouteSearchTargets.push('VehicleRentalStations');
- locationSearchTargets.push('VehicleRentalStations');
- }
- if (config.includeParkAndRideSuggestions) {
- stopAndRouteSearchTargets.push('ParkingAreas');
- locationSearchTargets.push('ParkingAreas');
+ if (config.locationSearchTargetsFromOTP) {
+ // configurable setup
+ locationSearchTargets = [
+ ...locationSearchTargets,
+ ...config.locationSearchTargetsFromOTP,
+ ];
+ } else {
+ // default setup
+ locationSearchTargets.push('Stops');
+
+ if (useCitybikes(config.vehicleRental?.networks, config)) {
+ stopAndRouteSearchTargets.push('VehicleRentalStations');
+ locationSearchTargets.push('VehicleRentalStations');
+ }
+ if (config.includeParkAndRideSuggestions) {
+ stopAndRouteSearchTargets.push('ParkingAreas');
+ locationSearchTargets.push('ParkingAreas');
+ }
}
const locationSearchTargetsMobile = [
...locationSearchTargets,
diff --git a/app/component/RouteNumberContainer.js b/app/component/RouteNumberContainer.js
index d1f06f72a6..eac349b498 100644
--- a/app/component/RouteNumberContainer.js
+++ b/app/component/RouteNumberContainer.js
@@ -13,6 +13,7 @@ const RouteNumberContainer = (
isCallAgency,
withBicycle,
occupancyStatus,
+ mode,
...props
},
{ config },
@@ -23,7 +24,7 @@ const RouteNumberContainer = (
className={className}
isCallAgency={isCallAgency || route.type === 715}
color={route.color ? `#${route.color}` : null}
- mode={route.mode}
+ mode={mode !== undefined ? mode : route.mode}
text={getLegText(route, config, interliningWithRoute)}
withBicycle={withBicycle}
occupancyStatus={occupancyStatus}
@@ -41,6 +42,7 @@ RouteNumberContainer.propTypes = {
fadeLong: PropTypes.bool,
withBicycle: PropTypes.bool,
occupancyStatus: PropTypes.string,
+ mode: PropTypes.string,
};
RouteNumberContainer.defaultProps = {
@@ -52,6 +54,7 @@ RouteNumberContainer.defaultProps = {
className: '',
withBicycle: false,
occupancyStatus: undefined,
+ mode: undefined,
};
RouteNumberContainer.contextTypes = {
diff --git a/app/component/StopTimetablePage.js b/app/component/StopTimetablePage.js
index d76b841353..75fb1983dd 100644
--- a/app/component/StopTimetablePage.js
+++ b/app/component/StopTimetablePage.js
@@ -28,12 +28,12 @@ class StopTimetablePage extends React.Component {
}
onDateChange = value => {
+ this.setState({ date: value });
this.props.relay.refetch(
{
date: value,
},
null,
- () => this.setState({ date: value }),
);
};
@@ -48,11 +48,8 @@ class StopTimetablePage extends React.Component {
);
}
diff --git a/app/component/SwipeableTabs.js b/app/component/SwipeableTabs.js
index b9ad5f119a..8efb2935f8 100644
--- a/app/component/SwipeableTabs.js
+++ b/app/component/SwipeableTabs.js
@@ -63,13 +63,8 @@ const handleKeyPress = (e, reactSwipeEl) => {
};
export default class SwipeableTabs extends React.Component {
- constructor(props) {
- super();
- this.state = { tabIndex: props.tabIndex };
- }
-
static propTypes = {
- tabIndex: PropTypes.number,
+ tabIndex: PropTypes.number.isRequired,
tabs: PropTypes.arrayOf(PropTypes.node).isRequired,
onSwipe: PropTypes.func.isRequired,
hideArrows: PropTypes.bool,
@@ -82,7 +77,6 @@ export default class SwipeableTabs extends React.Component {
static defaultProps = {
hideArrows: false,
navigationOnBottom: false,
- tabIndex: 0,
classname: undefined,
};
@@ -100,7 +94,7 @@ export default class SwipeableTabs extends React.Component {
}
tabBalls = tabsLength => {
- const tabIndex = parseInt(this.state.tabIndex, 10);
+ const tabIndex = parseInt(this.props.tabIndex, 10);
const onLeft = tabIndex;
const onRight = tabsLength - tabIndex - 1;
let tabBalls = [];
@@ -161,17 +155,15 @@ export default class SwipeableTabs extends React.Component {
)}
tabIndex={0}
className={`swipe-tab-ball ${
- index === this.state.tabIndex ? 'selected' : ''
+ index === this.props.tabIndex ? 'selected' : ''
} ${ball.smaller ? 'decreasing-small' : ''} ${
ball.small ? 'decreasing' : ''
} ${ball.hidden ? 'hidden' : ''}`}
onClick={() => {
- this.setState({ tabIndex: index });
this.props.onSwipe(index);
}}
onKeyDown={e => {
if (isKeyboardSelectionEvent(e)) {
- this.setState({ tabIndex: index });
this.props.onSwipe(index);
}
}}
@@ -247,7 +239,6 @@ export default class SwipeableTabs extends React.Component {
callback: i => {
// force transition after animation should be over because animation can randomly fail sometimes
setTimeout(() => {
- this.setState({ tabIndex: i });
this.props.onSwipe(i);
}, 300);
},
@@ -262,11 +253,7 @@ export default class SwipeableTabs extends React.Component {
)}
-
+
{this.props.classname === 'swipe-desktop-view' && (
)}
@@ -282,7 +269,7 @@ export default class SwipeableTabs extends React.Component {
{!hideArrows && (
@@ -322,7 +309,7 @@ export default class SwipeableTabs extends React.Component {
{!hideArrows && (
= tabs.length - 1),
+ active: !(disabled || this.props.tabIndex >= tabs.length - 1),
})}
>
= tabs.length - 1
+ disabled || this.props.tabIndex >= tabs.length - 1
? 'disabled'
: ''
}`}
@@ -361,7 +348,6 @@ export default class SwipeableTabs extends React.Component {
callback: i => {
// force transition after animation should be over because animation can randomly fail sometimes
setTimeout(() => {
- this.setState({ tabIndex: i });
this.props.onSwipe(i);
}, 300);
},
diff --git a/app/component/TerminalTimetablePage.js b/app/component/TerminalTimetablePage.js
index 4abc98025d..249ce062d7 100644
--- a/app/component/TerminalTimetablePage.js
+++ b/app/component/TerminalTimetablePage.js
@@ -34,12 +34,12 @@ class TerminalTimetablePage extends React.Component {
}
onDateChange = value => {
+ this.setState({ date: value });
this.props.relay.refetch(
{
date: value,
},
null,
- () => this.setState({ date: value }),
);
};
@@ -48,11 +48,8 @@ class TerminalTimetablePage extends React.Component {
);
}
diff --git a/app/component/Timetable.js b/app/component/Timetable.js
index 2ed3363d04..22c212ae06 100644
--- a/app/component/Timetable.js
+++ b/app/component/Timetable.js
@@ -82,19 +82,12 @@ class Timetable extends React.Component {
}),
).isRequired,
}).isRequired,
- propsForDateSelect: PropTypes.shape({
- startDate: PropTypes.string,
- selectedDate: PropTypes.string,
- onDateChange: PropTypes.func,
- }).isRequired,
- date: PropTypes.string,
+ startDate: PropTypes.string.isRequired,
+ onDateChange: PropTypes.func.isRequired,
+ date: PropTypes.string.isRequired,
language: PropTypes.string.isRequired,
};
- static defaultProps = {
- date: undefined,
- };
-
static contextTypes = {
router: routerShape.isRequired,
match: matchShape.isRequired,
@@ -183,7 +176,7 @@ class Timetable extends React.Component {
};
dateForPrinting = () => {
- const selectedDate = moment(this.props.propsForDateSelect.selectedDate);
+ const selectedDate = moment(this.props.date);
return (
@@ -331,9 +324,7 @@ class Timetable extends React.Component {
}${locationType.toLowerCase()}/${this.props.stop.gtfsId}`;
const timeTableRows = this.createTimeTableRows(timetableMap);
const timeDifferenceDays = moment
- .duration(
- moment(this.props.propsForDateSelect.selectedDate).diff(moment()),
- )
+ .duration(moment(this.props.date).diff(moment()))
.asDays();
return (
<>
@@ -349,10 +340,10 @@ class Timetable extends React.Component {
) : null}
{
- this.props.propsForDateSelect.onDateChange(e);
+ this.props.onDateChange(e);
const showRoutes = this.state.showRoutes.length
? this.state.showRoutes.join(',')
: undefined;
diff --git a/app/component/itinerary/BicycleLeg.js b/app/component/itinerary/BicycleLeg.js
index 33531a3a6b..4be92094c7 100644
--- a/app/component/itinerary/BicycleLeg.js
+++ b/app/component/itinerary/BicycleLeg.js
@@ -375,9 +375,9 @@ export default function BicycleLeg(
openSettings(true)}
+ onClick={() => openSettings(true, true)}
onKeyPress={e =>
- isKeyboardSelectionEvent(e) && openSettings(true)
+ isKeyboardSelectionEvent(e) && openSettings(true, true)
}
className="itinerary-transit-leg-route-bike"
>
diff --git a/app/component/itinerary/Itinerary.js b/app/component/itinerary/Itinerary.js
index ed32310c51..f23d646275 100644
--- a/app/component/itinerary/Itinerary.js
+++ b/app/component/itinerary/Itinerary.js
@@ -88,7 +88,7 @@ export function RouteLeg(
) {
const isCallAgency = isCallAgencyPickupType(leg);
let routeNumber;
- const mode = getRouteMode(leg.route);
+ const mode = getRouteMode(leg.route, config);
const getOccupancyStatus = () => {
if (hasOneTransitLeg) {
@@ -118,7 +118,7 @@ export function RouteLeg(
{
- const subpath = getSubPath('');
if (activeIndex === index) {
onSelectImmediately(index);
} else {
router.replace({
...match.location,
state: { selectedItineraryIndex: index },
- pathname: `${getItineraryPagePath(params.from, params.to)}${subpath}`,
});
addAnalyticsEvent({
diff --git a/app/component/itinerary/ItineraryPage.js b/app/component/itinerary/ItineraryPage.js
index a6281938b0..b36a6f5cd3 100644
--- a/app/component/itinerary/ItineraryPage.js
+++ b/app/component/itinerary/ItineraryPage.js
@@ -503,6 +503,9 @@ export default function ItineraryPage(props, context) {
laterEdges: [...state.laterEdges, ...edges],
});
}
+ if (arriveBy) {
+ resetItineraryPageSelection();
+ }
};
const onEarlier = async () => {
@@ -554,7 +557,6 @@ export default function ItineraryPage(props, context) {
return;
}
ariaRef.current = 'itinerary-page.itineraries-loaded';
-
const newState = {
...state,
loadingMore: undefined,
@@ -585,6 +587,9 @@ export default function ItineraryPage(props, context) {
earlierEdges: [...edges, ...state.earlierEdges],
});
}
+ if (!arriveBy) {
+ resetItineraryPageSelection();
+ }
};
// save url-defined location to old searches
@@ -867,7 +872,7 @@ export default function ItineraryPage(props, context) {
router.replace(newLocationState);
};
- const showSettingsPanel = open => {
+ const showSettingsPanel = (open, changeScooterSettings) => {
addAnalyticsEvent({
event: 'sendMatomoEvent',
category: 'ItinerarySettings',
@@ -880,6 +885,7 @@ export default function ItineraryPage(props, context) {
...settingsState,
settingsOpen: true,
settingsOnOpen: getSettings(config),
+ changeScooterSettings,
});
if (breakpoint !== 'large') {
router.push({
@@ -892,6 +898,17 @@ export default function ItineraryPage(props, context) {
}
return;
}
+ if (
+ settingsState.changeScooterSettings &&
+ settingsState.settingsOnOpen.scooterNetworks.length <
+ getSettings(config).scooterNetworks.length
+ ) {
+ addAnalyticsEvent({
+ category: 'ItinerarySettings',
+ action: 'SettingsEnableScooterNetwork',
+ name: 'AfterOnlyScooterRoutesFound',
+ });
+ }
const settingsChanged = !isEqual(
settingsState.settingsOnOpen,
@@ -903,6 +920,7 @@ export default function ItineraryPage(props, context) {
...settingsState,
settingsOpen: false,
settingsChanged,
+ changeScooterSettings: false,
});
if (settingsChanged && detailView) {
diff --git a/app/component/itinerary/ItineraryPageUtils.js b/app/component/itinerary/ItineraryPageUtils.js
index a6342199c3..e7bb0af43d 100644
--- a/app/component/itinerary/ItineraryPageUtils.js
+++ b/app/component/itinerary/ItineraryPageUtils.js
@@ -30,13 +30,25 @@ import { getTotalBikingDistance, compressLegs } from '../../util/legUtils';
export function getSelectedItineraryIndex(
{ pathname, state } = {},
edges = [],
- defaultValue = 0,
) {
+ // path defines the selection in detail view
+ const lastURLSegment = pathname?.split('/').pop();
+ if (lastURLSegment !== '') {
+ const index = Number(pathname?.split('/').pop());
+ if (!Number.isNaN(index)) {
+ if (index >= edges.length) {
+ return 0;
+ }
+ return index;
+ }
+ }
+
+ // in summary view, look the location state
if (state?.selectedItineraryIndex !== undefined) {
if (state.selectedItineraryIndex < edges.length) {
return state.selectedItineraryIndex;
}
- return defaultValue;
+ return 0;
}
/*
@@ -44,13 +56,6 @@ export function getSelectedItineraryIndex(
* page by an external link, we check if an itinerary selection is
* supplied in URL and make that the selection.
*/
- const lastURLSegment = Number(pathname?.split('/').pop());
- if (!Number.isNaN(lastURLSegment)) {
- if (lastURLSegment >= edges.length) {
- return defaultValue;
- }
- return lastURLSegment;
- }
return 0;
}
diff --git a/app/component/itinerary/LegInfo.js b/app/component/itinerary/LegInfo.js
index 214218f381..5bf0979d39 100644
--- a/app/component/itinerary/LegInfo.js
+++ b/app/component/itinerary/LegInfo.js
@@ -34,7 +34,10 @@ export default function LegInfo(
!constantOperationRoutes || !constantOperationRoutes[leg.route.gtfsId];
const mode = isCallAgency
? 'call'
- : getRouteMode({ mode: leg.mode, type: leg.route.type });
+ : getRouteMode(
+ { mode: leg.mode, type: leg.route.type, gtfsId: leg.route?.gtfsId },
+ config,
+ );
const capacity = getCapacityForLeg(config, leg);
let capacityTranslation;
if (capacity) {
diff --git a/app/component/itinerary/Legs.js b/app/component/itinerary/Legs.js
index 6a46710ac2..1da3999184 100644
--- a/app/component/itinerary/Legs.js
+++ b/app/component/itinerary/Legs.js
@@ -187,7 +187,14 @@ export default class Legs extends React.Component {
leg.mode === 'FUNICULAR') &&
!leg.interlineWithPreviousLeg
) {
- const mode = getRouteMode({ mode: leg.mode, type: leg.route?.type });
+ const mode = getRouteMode(
+ {
+ mode: leg.mode,
+ type: leg.route?.type,
+ gtfsId: leg.route?.gtfsId,
+ },
+ this.context.config,
+ );
legs.push();
} else if (leg.mode === 'AIRPLANE') {
legs.push(
diff --git a/app/component/itinerary/TransitLeg.js b/app/component/itinerary/TransitLeg.js
index 2cfa59d98b..3a83cd6134 100644
--- a/app/component/itinerary/TransitLeg.js
+++ b/app/component/itinerary/TransitLeg.js
@@ -505,7 +505,6 @@ class TransitLeg extends React.Component {
- {
- this.halo = el;
- }}
- positions={filteredPoints}
- className={`leg-halo ${className}`}
- weight={haloWeight}
- interactive={false}
- />
+ {displayHalo && (
+ {
+ this.halo = el;
+ }}
+ positions={filteredPoints}
+ className={`leg-halo ${className}`}
+ weight={haloWeight}
+ interactive={false}
+ />
+ )}
{
diff --git a/app/component/map/map.scss b/app/component/map/map.scss
index 9ed66118af..045121dd6e 100644
--- a/app/component/map/map.scss
+++ b/app/component/map/map.scss
@@ -830,6 +830,10 @@ div.origin-popup {
min-width: 50px;
height: 18px;
+ &.only-icon {
+ min-width: 0;
+ }
+
.wide {
min-width: 64px;
}
@@ -862,6 +866,10 @@ div.origin-popup {
color: $white;
}
+ .map-route-number.ferry-external {
+ color: $black;
+ }
+
&::before {
content: '';
width: 0;
@@ -878,12 +886,20 @@ div.origin-popup {
}
}
+.map .arrow-bottomLeft.only-icon div::before {
+ border-right: 10px solid transparent;
+}
+
.map .arrow-bottomRight {
display: flex;
align-items: center;
min-width: 50px;
height: 18px;
+ &.only-icon {
+ min-width: 0;
+ }
+
.wide {
min-width: 64px;
}
@@ -916,6 +932,10 @@ div.origin-popup {
color: $white;
}
+ .map-route-number.ferry-external {
+ color: $black;
+ }
+
&::before {
content: '';
width: 0;
@@ -931,12 +951,20 @@ div.origin-popup {
}
}
+.map .arrow-bottomRight.only-icon div::before {
+ border-right: 9px solid transparent;
+}
+
.map .arrow-topRight {
display: flex;
align-items: center;
min-width: 50px;
height: 18px;
+ &.only-icon {
+ min-width: 0;
+ }
+
.wide {
min-width: 64px;
}
@@ -969,6 +997,10 @@ div.origin-popup {
color: $white;
}
+ .map-route-number.ferry-external {
+ color: $black;
+ }
+
&::before {
content: '';
width: 0;
@@ -985,12 +1017,21 @@ div.origin-popup {
}
}
+.map .arrow-topRight.only-icon div::before {
+ border-left: 15px solid transparent;
+ right: 11px;
+}
+
.map .arrow-topLeft {
display: flex;
align-items: center;
min-width: 50px;
height: 18px;
+ &.only-icon {
+ min-width: 0;
+ }
+
.wide {
min-width: 64px;
}
@@ -1023,6 +1064,10 @@ div.origin-popup {
color: $white;
}
+ .map-route-number.ferry-external {
+ color: $black;
+ }
+
&::before {
content: '';
width: 0;
@@ -1045,6 +1090,11 @@ div.origin-popup {
min-width: 50px;
height: 18px;
+ &.only-icon {
+ min-width: 0;
+ align-items: normal;
+ }
+
.wide {
min-width: 64px;
}
@@ -1210,6 +1260,10 @@ div.origin-popup {
padding-right: 3px;
color: $white;
}
+
+ .map-route-number.ferry-external {
+ color: $black;
+ }
}
}
diff --git a/app/component/map/non-tile-layer/LegMarker.js b/app/component/map/non-tile-layer/LegMarker.js
index 92cf3672cd..2683d87d75 100644
--- a/app/component/map/non-tile-layer/LegMarker.js
+++ b/app/component/map/non-tile-layer/LegMarker.js
@@ -2,7 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react';
import Icon from '../../Icon';
import { isBrowser } from '../../../util/browser';
-import { legShape } from '../../../util/shapes';
+import { legShape, configShape } from '../../../util/shapes';
/* eslint-disable global-require */
@@ -28,10 +28,26 @@ class LegMarker extends React.Component {
style: undefined,
};
+ static contextTypes = {
+ config: configShape.isRequired,
+ };
+
// An arrow marker will be displayed if the normal marker can't fit
getLegMarker() {
const color = this.props.color ? this.props.color : 'currentColor';
const className = this.props.wide ? 'wide' : '';
+ // Do not display route number if it is an external route and the route number is empty.
+ const displayRouteNumber = !(
+ this.context.config.externalFeedIds !== undefined &&
+ this.props.mode.includes('external') &&
+ this.props.leg.name === ''
+ );
+ const routeNumber = displayRouteNumber
+ ? `${
+ this.props.leg.name
+ }
+ ${this.props.leg.name.toLowerCase()}`
+ : '';
return (
${
- this.props.leg.name
- }
- ${this.props.leg.name.toLowerCase()}
+ ${routeNumber}
`,
className: `${
this.props.style ? `arrow-${this.props.style}` : 'legmarker'
- } ${this.props.mode}`,
+ } ${this.props.mode} ${displayRouteNumber ? '' : 'only-icon'}`,
iconSize: null,
})}
zIndexOffset={this.props.zIndexOffset}
diff --git a/app/configurations/config.hsl.js b/app/configurations/config.hsl.js
index b65ab78bca..ee62060eba 100644
--- a/app/configurations/config.hsl.js
+++ b/app/configurations/config.hsl.js
@@ -90,6 +90,7 @@ export default {
useRoutingFeedbackPrompt: true,
feedIds: ['HSL', 'HSLlautta', 'Sipoo'],
+ externalFeedIds: ['HSLlautta'],
showHSLTracking: false,
allowLogin: true,
@@ -520,15 +521,15 @@ export default {
scooterInfoLink: {
fi: {
text: 'Potkulaudat',
- url: 'https://www.hsl.fi/hsl/uutiset/teemat/potkulaudat',
+ url: 'https://www.hsl.fi/reittiopas_potkulaudat',
},
en: {
text: 'Scooters',
- url: 'https://www.hsl.fi/hsl/uutiset/teemat/potkulaudat',
+ url: 'https://www.hsl.fi/en/journey_planner_scooters',
},
sv: {
text: 'Elsparkcyklar',
- url: 'https://www.hsl.fi/hsl/uutiset/teemat/potkulaudat',
+ url: 'https://www.hsl.fi/sv/reseplaneraren_sparkcyklar',
},
},
maxMinutesToRentalJourneyEnd: 240,
@@ -580,7 +581,7 @@ export default {
showSimilarRoutesOnRouteDropDown: true,
useRealtimeTravellerCapacities: true,
- navigation: true,
+ navigation: false,
stopCard: {
header: {
diff --git a/app/configurations/config.jyvaskyla.js b/app/configurations/config.jyvaskyla.js
index 3a777ebae2..af6d6ecd6d 100644
--- a/app/configurations/config.jyvaskyla.js
+++ b/app/configurations/config.jyvaskyla.js
@@ -183,7 +183,7 @@ export default configMerger(walttiConfig, {
for (let i = 0; i < ticket.length; i++) {
zones += `0${ticket.charCodeAt(i) - 64}`; // eslint-disable
}
- return `https://waltti.fi/walttiappfeat/busTicket/?operator=50209&ticketType=single&customerGroup=adult&zones=${zones}`;
+ return `https://waltti.fi/walttiapp/busTicket/?operator=50209&ticketType=single&customerGroup=adult&zones=${zones}`;
},
fareMapping: function mapFareId(fareId) {
diff --git a/app/configurations/config.matka.js b/app/configurations/config.matka.js
index cb93310f92..00fc3035bf 100644
--- a/app/configurations/config.matka.js
+++ b/app/configurations/config.matka.js
@@ -90,6 +90,7 @@ export default {
'VARELY',
'Harma',
'PohjolanMatka',
+ 'Korsisaari',
],
additionalFeedIds: {
@@ -176,6 +177,20 @@ export default {
...KotkaConfig.vehicleRental.networks,
...KouvolaConfig.vehicleRental.networks,
},
+ scooterInfoLink: {
+ fi: {
+ text: 'Potkulaudat',
+ url: 'https://www.fintraffic.fi/fi/uutiset/sahkopotkulaudat-nyt-mukana-opasmatkafi-reittioppaassa',
+ },
+ en: {
+ text: 'Scooters',
+ url: 'https://www.fintraffic.fi/en/news/electric-scooters-now-included-opasmatkafi-journey-planner',
+ },
+ sv: {
+ text: 'Elsparkcyklar',
+ url: 'https://www.fintraffic.fi/sv/nyheter/elsparkcyklarna-finns-nu-med-i-reseplaneraren-opasmatkafi',
+ },
+ },
},
getAutoSuggestIcons: {
diff --git a/app/configurations/realtimeUtils.js b/app/configurations/realtimeUtils.js
index e05d29778b..0c67fecc0b 100644
--- a/app/configurations/realtimeUtils.js
+++ b/app/configurations/realtimeUtils.js
@@ -146,6 +146,7 @@ export default {
VARELY: walttiMqtt,
PohjolanMatka: elyMqtt(true),
Harma: elyMqtt(false),
+ Korsisaari: walttiMqtt,
FOLI: {
mqttTopicResolver: function mqttTopicResolver(
route,
diff --git a/app/translations.js b/app/translations.js
index 0b149cf1c8..05acb751a8 100644
--- a/app/translations.js
+++ b/app/translations.js
@@ -1071,6 +1071,8 @@ const translations = {
'favourite-failed-text': 'Please try again in a while.',
'favourite-target': 'Favorite location',
ferry: 'Ferry',
+ 'ferry-external': 'Ferry',
+ 'ferry-external-with-route-number': 'Ferry {routeNumber} {headSign}',
'ferry-with-route-number': 'Ferry {routeNumber} {headSign}',
'fetch-new-route': 'Fetch a new route',
'few-seats-available': 'Few seats available',
@@ -1275,6 +1277,7 @@ const translations = {
'modes.to-bike-park': 'bike park',
'modes.to-bus': 'bus stop',
'modes.to-car-park': 'car park',
+ 'modes.to-ferry': 'ferry pier',
'modes.to-place': 'destination',
'modes.to-rail': 'train station',
'modes.to-scooter': 'to scooter',
@@ -2305,6 +2308,8 @@ const translations = {
'favourite-failed-text': 'Yritä hetken päästä uudelleen.',
'favourite-target': 'Suosikkikohde',
ferry: 'Lautta',
+ 'ferry-external': 'Lautta',
+ 'ferry-external-with-route-number': 'Lautta {routeNumber} {headSign}',
'ferry-with-route-number': 'Lautta {routeNumber} {headSign}',
'fetch-new-route': 'Hae uusi reitti',
'few-seats-available': 'Joitakin istumapaikkoja vapaana',
@@ -2502,6 +2507,7 @@ const translations = {
'modes.to-bike-park': 'liityntäpyöräparkkiin',
'modes.to-bus': 'bussipysäkille',
'modes.to-car-park': 'liityntäpysäköintiin',
+ 'modes.to-ferry': 'lauttalaiturille',
'modes.to-place': 'kohteeseen',
'modes.to-rail': 'juna-asemalle',
'modes.to-scooter': 'potkulaudalle',
@@ -5177,6 +5183,8 @@ const translations = {
'favourite-failed-text': 'Försök på nytt senare.',
'favourite-target': 'Favoritdestination',
ferry: 'Färja',
+ 'ferry-external': 'Färja',
+ 'ferry-external-with-route-number': 'Färja {routeNumber} {headSign}',
'ferry-with-route-number': 'Färja {routeNumber} {headSign}',
'fetch-new-route': 'Sök en ny rutt',
'few-seats-available': 'Några sittplatser',
@@ -5377,6 +5385,7 @@ const translations = {
'modes.to-bike-park': 'anslutningsparkering för cyklar',
'modes.to-bus': 'busshållplats',
'modes.to-car-park': 'infartsparkering',
+ 'modes.to-ferry': 'färjekajen',
'modes.to-place': 'destination',
'modes.to-rail': 'tågstation',
'modes.to-scooter': 'elsparkcykel',
diff --git a/app/util/feedScopedIdUtils.js b/app/util/feedScopedIdUtils.js
index 410d755561..23de971504 100644
--- a/app/util/feedScopedIdUtils.js
+++ b/app/util/feedScopedIdUtils.js
@@ -14,3 +14,30 @@ export const getIdWithoutFeed = feedScopedId => {
}
return feedScopedId.split(':')[1];
};
+
+/**
+ * Returns feedId without the objectId.
+ *
+ * @param {String} feedScopedId should be in feedId:objectId format.
+ */
+export const getFeedWithoutId = feedScopedId => {
+ if (!feedScopedId) {
+ return undefined;
+ }
+ if (feedScopedId.indexOf(':') === -1) {
+ return feedScopedId;
+ }
+ return feedScopedId.split(':')[0];
+};
+
+/**
+ * Returns boolean indicating whether the feed is external or not.
+ */
+export const isExternalFeed = (feedId, config) => {
+ return (
+ config !== undefined &&
+ config.externalFeedIds !== undefined &&
+ feedId !== undefined &&
+ config.externalFeedIds.includes(feedId)
+ );
+};
diff --git a/app/util/modeUtils.js b/app/util/modeUtils.js
index aee5f90a6e..768aabba92 100644
--- a/app/util/modeUtils.js
+++ b/app/util/modeUtils.js
@@ -7,6 +7,7 @@ import { isInBoundingBox } from './geo-utils';
import { addAnalyticsEvent } from './analyticsUtils';
import { ExtendedRouteTypes, TransportMode } from '../constants';
import { isDevelopmentEnvironment } from './envUtils';
+import { getFeedWithoutId, isExternalFeed } from './feedScopedIdUtils';
function seasonMs(ddmmyyyy) {
const parts = ddmmyyyy.split('.');
@@ -113,7 +114,7 @@ export function getTransportModes(config) {
};
}
-export function getRouteMode(route) {
+export function getRouteMode(route, config) {
switch (route.type) {
case ExtendedRouteTypes.BusExpress:
return 'bus-express';
@@ -122,7 +123,9 @@ export function getRouteMode(route) {
case ExtendedRouteTypes.SpeedTram:
return 'speedtram';
default:
- return route.mode?.toLowerCase();
+ return isExternalFeed(getFeedWithoutId(route?.gtfsId), config)
+ ? `${route.mode?.toLowerCase()}-external`
+ : route.mode?.toLowerCase();
}
}
diff --git a/app/util/vehicleRentalUtils.js b/app/util/vehicleRentalUtils.js
index 3e6ec7cc5d..242489e5f9 100644
--- a/app/util/vehicleRentalUtils.js
+++ b/app/util/vehicleRentalUtils.js
@@ -131,33 +131,30 @@ const addAnalytics = (action, name) => {
* Updates the list of allowed networks either by removing or adding.
* Note: legacy settings had network names always in uppercase letters.
*
- * @param currentSettings the current settings
- * @param newValue the network to be added/removed
- * @param config The configuration for the software installation
- * @param isUsingCitybike if citybike is enabled
+ * @param networks the previously selected networks
+ * @param networkName the network to be added/removed
+ * @param type the type of the network
* @returns the updated citybike networks
*/
-export const updateVehicleNetworks = (currentSettings, newValue) => {
- let chosenNetworks;
+export const updateVehicleNetworks = (networks, networkName, type) => {
+ let updatedNetworks;
+ let toggleAction;
- if (currentSettings) {
- chosenNetworks = currentSettings.find(
- o => o.toLowerCase() === newValue.toLowerCase(),
- )
- ? without(currentSettings, newValue, newValue.toUpperCase())
- : currentSettings.concat([newValue]);
+ if (networks.find(o => o.toLowerCase() === networkName.toLowerCase())) {
+ updatedNetworks = without(networks, networkName, networkName.toUpperCase());
+ toggleAction = 'Disable';
} else {
- chosenNetworks = [newValue];
+ updatedNetworks = networks.concat([networkName]);
+ toggleAction = 'Enable';
}
- if (Array.isArray(currentSettings) && Array.isArray(chosenNetworks)) {
- const action = `Settings${
- currentSettings.length > chosenNetworks.length ? 'Disable' : 'Enable'
- }CityBikeNetwork`;
- addAnalytics(action, newValue);
- }
- return chosenNetworks;
+ const action = `Settings${toggleAction}${
+ type === 'citybike' ? 'CityBikeNetwork' : 'ScooterNetwork'
+ }`;
+ addAnalytics(action, networkName);
+
+ return updatedNetworks;
};
export const getVehicleMinZoomOnStopsNearYou = (config, override) => {
@@ -184,11 +181,17 @@ export const hasVehicleRentalCode = rentalId => {
};
export const mapVehicleRentalFromStore = vehicleRentalStation => {
- const network = vehicleRentalStation.networks[0];
+ const originalId = vehicleRentalStation.stationId;
+ const network =
+ vehicleRentalStation.networks?.[0] || originalId.split(':')[0];
+ const stationId = originalId.startsWith(network)
+ ? originalId
+ : `${network}:${originalId}`;
+
const newStation = {
...vehicleRentalStation,
network,
- stationId: `${network}:${vehicleRentalStation.stationId}`,
+ stationId,
};
delete newStation.networks;
return newStation;
diff --git a/package.json b/package.json
index f3df387584..201a926f22 100644
--- a/package.json
+++ b/package.json
@@ -147,7 +147,7 @@
"babel-plugin-relay": "16.2.0",
"body-parser": "1.19.0",
"classnames": "2.2.6",
- "connect-redis": "7.1.1",
+ "connect-redis": "5.0.0",
"cookie-parser": "1.4.5",
"debug": "4.3.4",
"esm": "3.2.25",
@@ -204,7 +204,7 @@
"react-swipe": "6.0.4",
"react-truncate-markup": "5.1.0",
"recompose": "0.30.0",
- "redis": "4.6.13",
+ "redis": "2.8.0",
"relay-runtime": "16.2.0",
"serialize-javascript": "4.0.0",
"suncalc": "1.8.0",
diff --git a/sass/base/_helper-classes.scss b/sass/base/_helper-classes.scss
index 740b9e66e5..ec34c58e18 100644
--- a/sass/base/_helper-classes.scss
+++ b/sass/base/_helper-classes.scss
@@ -44,6 +44,10 @@
color: $ferry-color;
}
+.ferry-external {
+ color: $external-feed-color;
+}
+
.funicular {
color: $funicular-color;
}
diff --git a/sass/themes/default/_theme.scss b/sass/themes/default/_theme.scss
index 196c8c7a0d..9fd716da7c 100644
--- a/sass/themes/default/_theme.scss
+++ b/sass/themes/default/_theme.scss
@@ -84,6 +84,7 @@ $bicycle-color: #666;
$car-color: #333;
$scooter-color: #c5cad2;
$call-agency-color: #666;
+$external-feed-color: #c5cad2;
/* Fonts */
$font-family: 'Roboto', arial, georgia, serif;
diff --git a/scripts/README.md b/scripts/README.md
index 0613f70d9f..f40d37425c 100644
--- a/scripts/README.md
+++ b/scripts/README.md
@@ -1,16 +1,20 @@
# Using scripts
-## Before using:
+## Before using
```
source ui.sh
```
## Usage examples
-Using remote instance of OTP with subscription key.
+Using remote instance of OTP with subscription key:
```
SUBSCRIPTION_KEY= ui hsl
```
-Using local instance of OTP on port `9080`.
+Using local instance of OTP on port `9080`:
+```
+SUBSCRIPTION_KEY= uiotp matka
+```
+In case you do not need features usable with a subscription key when running a local instance of OTP on port `9080`:
+```
+NO_SUBSCRIPTION_KEY=true uiotp matka
```
-uiotp matka
-```
\ No newline at end of file
diff --git a/scripts/ui.sh b/scripts/ui.sh
index 3c1cd304ea..cfcaf235cb 100755
--- a/scripts/ui.sh
+++ b/scripts/ui.sh
@@ -1,6 +1,11 @@
#!/usr/bin/env bash
ui () {
+ if [ "$SUBSCRIPTION_KEY" = "" -a "$NO_SUBSCRIPTION_KEY" != "true" ]; then
+ echo "In order to use the UI you need to set the SUBSCRIPTION_KEY environment variable."
+ echo "If you want to run the UI without a subscription key, set NO_SUBSCRIPTION_KEY=true."
+ return 1 2>/dev/null
+ fi
CONFIG=$1 API_SUBSCRIPTION_QUERY_PARAMETER_NAME=digitransit-subscription-key API_SUBSCRIPTION_HEADER_NAME=digitransit-subscription-key API_SUBSCRIPTION_TOKEN=$SUBSCRIPTION_KEY yarn run dev
}
diff --git a/server/passport-openid-connect/openidConnect.js b/server/passport-openid-connect/openidConnect.js
index 16b86dbb05..df6dcd677e 100644
--- a/server/passport-openid-connect/openidConnect.js
+++ b/server/passport-openid-connect/openidConnect.js
@@ -4,7 +4,7 @@ const session = require('express-session');
const redis = require('redis');
const axios = require('axios');
const moment = require('moment');
-const RedisStore = require('connect-redis').default;
+const RedisStore = require('connect-redis')(session);
const LoginStrategy = require('./Strategy').Strategy;
const clearAllUserSessions = false; // set true if logout should erase all user's sessions
@@ -28,13 +28,13 @@ export default function setUpOIDC(app, port, indexPath, hostnames) {
const RedisHost = process.env.REDIS_HOST || 'localhost';
const RedisPort = process.env.REDIS_PORT || 6379;
- const RedisProtocol = process.env.REDIS_HOST ? 'rediss' : 'redis';
- const RedisURL = `${RedisProtocol}://${RedisHost}:${RedisPort}`;
const RedisKey = process.env.REDIS_KEY;
const RedisClient = RedisKey
- ? redis.createClient({ url: RedisURL, password: RedisKey })
- : redis.createClient({ url: RedisURL });
- RedisClient.connect().catch(console.error);
+ ? redis.createClient(RedisPort, RedisHost, {
+ auth_pass: RedisKey,
+ tls: { servername: RedisHost },
+ })
+ : redis.createClient(RedisPort, RedisHost);
const redirectUris = hostnames.map(host => `${host}${callbackPath}`);
const postLogoutRedirectUris = hostnames.map(
@@ -61,7 +61,7 @@ export default function setUpOIDC(app, port, indexPath, hostnames) {
console.log(`adding session for used ${userId} id ${sessionId}`);
}
if (clearAllUserSessions) {
- RedisClient.sAdd(`sessions-${userId}`, sessionId);
+ RedisClient.sadd(`sessions-${userId}`, sessionId);
}
},
});
@@ -125,6 +125,8 @@ export default function setUpOIDC(app, port, indexPath, hostnames) {
session({
secret: process.env.SESSION_SECRET || 'reittiopas_secret',
store: new RedisStore({
+ host: RedisHost,
+ port: RedisPort,
client: RedisClient,
ttl: 60 * 60 * 24 * 60,
}),
@@ -204,7 +206,7 @@ export default function setUpOIDC(app, port, indexPath, hostnames) {
}
const sessions = `sessions-${req.session.userId}`;
req.logout({}, () => {
- RedisClient.sMembers(sessions).then(sessionIds => {
+ RedisClient.smembers(sessions, function (err, sessionIds) {
req.session.destroy(function () {
res.clearCookie('connect.sid');
if (sessionIds && sessionIds.length > 0) {
diff --git a/static/assets/svg-sprite.default.svg b/static/assets/svg-sprite.default.svg
index b89e0b5d9a..665ff70247 100644
--- a/static/assets/svg-sprite.default.svg
+++ b/static/assets/svg-sprite.default.svg
@@ -1841,7 +1841,7 @@
-
+
diff --git a/static/assets/svg-sprite.hsl.svg b/static/assets/svg-sprite.hsl.svg
index c97183ba87..00677c698d 100644
--- a/static/assets/svg-sprite.hsl.svg
+++ b/static/assets/svg-sprite.hsl.svg
@@ -100,6 +100,10 @@
+
+
+
+
diff --git a/test/unit/component/Timetable.test.js b/test/unit/component/Timetable.test.js
index 38266b887f..45290d2128 100644
--- a/test/unit/component/Timetable.test.js
+++ b/test/unit/component/Timetable.test.js
@@ -12,11 +12,8 @@ import * as timetables from '../../../app/configurations/timetableConfigUtils';
const stopIdNumber = '1140199';
const props = {
- propsForDateSelect: {
- startDate: '20190110',
- selectedDate: '20190110',
- onDateChange: () => {},
- },
+ startDate: '20190110',
+ onDateChange: () => {},
stop: {
gtfsId: `HSL:${stopIdNumber}`,
locationType: 'STOP',
diff --git a/yarn.lock b/yarn.lock
index 1b00cb003e..3d6a826414 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -5453,62 +5453,6 @@ __metadata:
languageName: node
linkType: hard
-"@redis/bloom@npm:1.2.0":
- version: 1.2.0
- resolution: "@redis/bloom@npm:1.2.0"
- peerDependencies:
- "@redis/client": ^1.0.0
- checksum: 8c214227287d6b278109098bca00afc601cf84f7da9c6c24f4fa7d3854b946170e5893aa86ed607ba017a4198231d570541c79931b98b6d50b262971022d1d6c
- languageName: node
- linkType: hard
-
-"@redis/client@npm:1.5.14":
- version: 1.5.14
- resolution: "@redis/client@npm:1.5.14"
- dependencies:
- cluster-key-slot: 1.1.2
- generic-pool: 3.9.0
- yallist: 4.0.0
- checksum: f401997c6df92055c1a59385ed2fed7ee9295860f39935821107ea2570f76168dd1b25b71a3b37b9bbfaba26a9d18080d6bcd101a4bfc3852f72cc20576c6e7d
- languageName: node
- linkType: hard
-
-"@redis/graph@npm:1.1.1":
- version: 1.1.1
- resolution: "@redis/graph@npm:1.1.1"
- peerDependencies:
- "@redis/client": ^1.0.0
- checksum: caf9b9a3ff82a08ae543c356a3fed548399ae79aba5ed08ce6cf1b522b955eb5cee4406b0ed0c6899345f8fbc06dfd6cd51304ae8422c3ebbc468f53294dc509
- languageName: node
- linkType: hard
-
-"@redis/json@npm:1.0.6":
- version: 1.0.6
- resolution: "@redis/json@npm:1.0.6"
- peerDependencies:
- "@redis/client": ^1.0.0
- checksum: 9fda29abc339c72593f34a23f8023b715c1f8f3d73f7c59889af02f25589bac2ad57073ad08d0b8da42cd8c258665a7b38d39e761e92945cc27aca651c8a93a5
- languageName: node
- linkType: hard
-
-"@redis/search@npm:1.1.6":
- version: 1.1.6
- resolution: "@redis/search@npm:1.1.6"
- peerDependencies:
- "@redis/client": ^1.0.0
- checksum: 0d87e6a9e40e62e46064ccfccca9a5ba7ce608890740415008acb1e83a02690edf37ac1ee878bcc0702d30a2eeba7776e7b467b71f87d8e7b278f38637161e16
- languageName: node
- linkType: hard
-
-"@redis/time-series@npm:1.0.5":
- version: 1.0.5
- resolution: "@redis/time-series@npm:1.0.5"
- peerDependencies:
- "@redis/client": ^1.0.0
- checksum: 6bbdb0b793dcbd13518aa60a09a980f953554e4c745bfacc1611baa8098f360e0378e8ee6b7faf600a67f1de83f4b68bbec6f95a0740faee6164c14be3a30752
- languageName: node
- linkType: hard
-
"@repeaterjs/repeater@npm:3.0.4":
version: 3.0.4
resolution: "@repeaterjs/repeater@npm:3.0.4"
@@ -9272,13 +9216,6 @@ __metadata:
languageName: node
linkType: hard
-"cluster-key-slot@npm:1.1.2":
- version: 1.1.2
- resolution: "cluster-key-slot@npm:1.1.2"
- checksum: be0ad2d262502adc998597e83f9ded1b80f827f0452127c5a37b22dfca36bab8edf393f7b25bb626006fb9fb2436106939ede6d2d6ecf4229b96a47f27edd681
- languageName: node
- linkType: hard
-
"co@npm:^4.6.0":
version: 4.6.0
resolution: "co@npm:4.6.0"
@@ -9688,12 +9625,10 @@ __metadata:
languageName: node
linkType: hard
-"connect-redis@npm:7.1.1":
- version: 7.1.1
- resolution: "connect-redis@npm:7.1.1"
- peerDependencies:
- express-session: ">=1"
- checksum: ac91ee818d0f467866b6982f66b3423fee58de9da3562618f6d1df2ddeea426354c1efe70b3f799be1b52e3cc67f2043b9ae203678fd3ff9db5dff44b078f0ca
+"connect-redis@npm:5.0.0":
+ version: 5.0.0
+ resolution: "connect-redis@npm:5.0.0"
+ checksum: 0eca205a42b68842c6b461d3cb51bde2cbc99984a0af51e99701f07a55209f3f60e1e34dd6d6cc81aa4fd462cd2d9881bf9b2f44ca3bbcd4b496514822a5e585
languageName: node
linkType: hard
@@ -11263,7 +11198,7 @@ __metadata:
chai: 4.3.7
classnames: 2.2.6
compression-webpack-plugin: 5.0.2
- connect-redis: 7.1.1
+ connect-redis: 5.0.0
cookie-parser: 1.4.5
copy-webpack-plugin: 6.1.0
css-loader: ^5.2.7
@@ -11364,7 +11299,7 @@ __metadata:
react-swipe: 6.0.4
react-truncate-markup: 5.1.0
recompose: 0.30.0
- redis: 4.6.13
+ redis: 2.8.0
relay-compiler: 16.2.0
relay-runtime: 16.2.0
rollup: 2.35.1
@@ -11712,6 +11647,13 @@ __metadata:
languageName: node
linkType: hard
+"double-ended-queue@npm:^2.1.0-0":
+ version: 2.1.0-0
+ resolution: "double-ended-queue@npm:2.1.0-0"
+ checksum: 3030cf9dcf6f8e7d8cb6ae5b7304890445d7c32233a614e400ba7b378086ad76f5822d0e501afd5ffe0af1de4bcb842fa23d4c79174d54f6566399435fafc271
+ languageName: node
+ linkType: hard
+
"dset@npm:^3.1.2":
version: 3.1.3
resolution: "dset@npm:3.1.3"
@@ -14070,13 +14012,6 @@ __metadata:
languageName: node
linkType: hard
-"generic-pool@npm:3.9.0":
- version: 3.9.0
- resolution: "generic-pool@npm:3.9.0"
- checksum: 3d89e9b2018d2e3bbf44fec78c76b2b7d56d6a484237aa9daf6ff6eedb14b0899dadd703b5d810219baab2eb28e5128fb18b29e91e602deb2eccac14492d8ca8
- languageName: node
- linkType: hard
-
"genfun@npm:^5.0.0":
version: 5.0.0
resolution: "genfun@npm:5.0.0"
@@ -24144,17 +24079,28 @@ __metadata:
languageName: node
linkType: hard
-"redis@npm:4.6.13":
- version: 4.6.13
- resolution: "redis@npm:4.6.13"
+"redis-commands@npm:^1.2.0":
+ version: 1.7.0
+ resolution: "redis-commands@npm:1.7.0"
+ checksum: d1ff7fbcb5e54768c77f731f1d49679d2a62c3899522c28addb4e2e5813aea8bcac3f22519d71d330224c3f2937f935dfc3d8dc65e90db0f5fe22dc2c1515aa7
+ languageName: node
+ linkType: hard
+
+"redis-parser@npm:^2.6.0":
+ version: 2.6.0
+ resolution: "redis-parser@npm:2.6.0"
+ checksum: 8d4936875e39d56a951e0bbb6653b4da1f7fdd727552c89561c3c78e7ffeb9c3e8820a78454e939b74d1ba20996d62ac179b4fc39d07340d10f8d52740399422
+ languageName: node
+ linkType: hard
+
+"redis@npm:2.8.0":
+ version: 2.8.0
+ resolution: "redis@npm:2.8.0"
dependencies:
- "@redis/bloom": 1.2.0
- "@redis/client": 1.5.14
- "@redis/graph": 1.1.1
- "@redis/json": 1.0.6
- "@redis/search": 1.1.6
- "@redis/time-series": 1.0.5
- checksum: 10150ec30f1f89e47cec41c27dc77a99c78a0b078735de4fc9b79c2a779787d52274c64c52004af161b95b9729067671a4cfca95257719968accb30057856a09
+ double-ended-queue: ^2.1.0-0
+ redis-commands: ^1.2.0
+ redis-parser: ^2.6.0
+ checksum: e44dc50a9a92ede2c95b3166482a4b04373853fdbb72ab138e834b868a3cd446798742fc0f85de2d5e442f9c86f58e1250637ea71af3ad5527478d05ef430079
languageName: node
linkType: hard
@@ -29554,13 +29500,6 @@ __metadata:
languageName: node
linkType: hard
-"yallist@npm:4.0.0, yallist@npm:^4.0.0":
- version: 4.0.0
- resolution: "yallist@npm:4.0.0"
- checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5
- languageName: node
- linkType: hard
-
"yallist@npm:^2.1.2":
version: 2.1.2
resolution: "yallist@npm:2.1.2"
@@ -29575,6 +29514,13 @@ __metadata:
languageName: node
linkType: hard
+"yallist@npm:^4.0.0":
+ version: 4.0.0
+ resolution: "yallist@npm:4.0.0"
+ checksum: 343617202af32df2a15a3be36a5a8c0c8545208f3d3dfbc6bb7c3e3b7e8c6f8e7485432e4f3b88da3031a6e20afa7c711eded32ddfb122896ac5d914e75848d5
+ languageName: node
+ linkType: hard
+
"yaml@npm:^1.10.0, yaml@npm:^1.10.2, yaml@npm:^1.7.2":
version: 1.10.2
resolution: "yaml@npm:1.10.2"