Skip to content

Commit

Permalink
Merge pull request #1304 from opentripplanner/trip-sharing
Browse files Browse the repository at this point in the history
Trip sharing
  • Loading branch information
binh-dam-ibigroup authored Dec 23, 2024
2 parents 3a55bf6 + 55ed3b7 commit 576b26b
Show file tree
Hide file tree
Showing 19 changed files with 449 additions and 93 deletions.
7 changes: 7 additions & 0 deletions i18n/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -533,6 +533,9 @@ components:
SavedTripEditor:
deleteSavedTrip: Delete saved trip
editSavedTrip: Edit saved trip
readOnlyBanner: >-
{creator} created and added you to this trip. Therefore, you cannot make
changes.
saveNewTrip: Save new trip
travelCompanions: Travel companions
tripInformation: Trip information
Expand Down Expand Up @@ -639,6 +642,10 @@ components:
tripNotAvailableOnDay: Trip not available on {repeatedDay}
unsavedChangesExistingTrip: You haven't saved your trip yet. If you leave, changes will be lost.
unsavedChangesNewTrip: You haven't saved your new trip yet. If you leave, it will be lost.
TripCompanionsPane:
companionLabel: "Companion on this trip:"
observersLabel: "Observers watching this trip:"
primaryLabel: "Primary traveler: "
TripNotificationsPane:
advancedSettings: Advanced settings
altRouteRecommended: An alternative route or transfer point is recommended
Expand Down
10 changes: 8 additions & 2 deletions i18n/fr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -358,8 +358,7 @@ components:
mobilityLimitations: "Handicaps moteurs : "
planTripDescription: >-
Vous pouvez rechercher des trajets adaptés au profil mobilité des
personnes que vous accompagnez. Pour ajouter des personnes
accompagnatrices, allez dans <manageLink>Préférences</manageLink>.
personnes que vous accompagnez.
visionLimitations: "Handicaps visuels : "
dropdownLabel: "Profil à utiliser :"
intro: >-
Expand Down Expand Up @@ -555,6 +554,9 @@ components:
SavedTripEditor:
deleteSavedTrip: Supprimer ce trajet
editSavedTrip: Modifier un trajet enregistré
readOnlyBanner: >-
{creator} a créé et vous a ajouté sur ce trajet. Vous ne pouvez donc pas
faire de modifications.
saveNewTrip: Enregistrer un nouveau trajet
travelCompanions: Accompagnateurs
tripInformation: Informations sur le trajet
Expand Down Expand Up @@ -672,6 +674,10 @@ components:
unsavedChangesNewTrip: >-
Vous n'avez pas encore enregistré votre nouveau trajet. Si vous annulez,
ce trajet sera perdu.
TripCompanionsPane:
companionLabel: "Accompagnateurs sur ce trajet :"
observersLabel: "Observateurs suivant ce trajet :"
primaryLabel: "Voyageur principal : "
TripNotificationsPane:
advancedSettings: Paramètres avancés
altRouteRecommended: Un·e autre trajet ou correspondance est conseillé·e
Expand Down
3 changes: 2 additions & 1 deletion i18n/i18n-exceptions.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,5 +52,6 @@
"low-vision",
"legally-blind"
]
}
},
"ignoredIds": ["otpUi.TripDetails.title"]
}
12 changes: 8 additions & 4 deletions lib/actions/apiV2.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import clone from 'clone'
import coreUtils from '@opentripplanner/core-utils'

import { checkForRouteModeOverride } from '../util/config'
import { convertToPlace, getPersistenceMode } from '../util/user'
import {
convertToPlace,
getPersistenceMode,
getUserWithEmail
} from '../util/user'
import { FETCH_STATUS } from '../util/constants'
import {
generateModeSettingValues,
Expand Down Expand Up @@ -1026,10 +1030,10 @@ export function routingQuery(searchId = null, updateSearchInReducer) {
...currentQuery,
numItineraries: numItineraries || getDefaultNumItineraries(config)
}
if (config.mobilityProfile) {
if (config.mobilityProfile && loggedInUser) {
baseQuery.mobilityProfile =
currentQuery.mobilityProfile ||
loggedInUser?.mobilityProfile?.mobilityMode
getUserWithEmail(loggedInUser.dependentsInfo, currentQuery.forEmail)
?.mobilityMode || loggedInUser.mobilityProfile?.mobilityMode
}
// Generate combinations if the modes for query are not specified in the query
// FIXME: BICYCLE_RENT does not appear in this list unless TRANSIT is also enabled.
Expand Down
22 changes: 9 additions & 13 deletions lib/components/form/advanced-settings-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import { AppReduxState } from '../../util/state-types'
import { blue, getBaseColor } from '../util/colors'
import { ComponentContext } from '../../util/contexts'
import { generateModeSettingValues } from '../../util/api'
import { getDependentName } from '../../util/user'
import { User } from '../user/types'
import Link from '../util/link'

import {
addCustomSettingLabels,
Expand Down Expand Up @@ -178,11 +178,7 @@ const AdvancedSettingsPanel = ({
const intl = useIntl()
const [closingBySave, setClosingBySave] = useState(false)
const [selectedMobilityProfile, setSelectedMobilityProfile] =
useState<string>(
currentQuery.mobilityProfile ||
loggedInUser?.mobilityProfile?.mobilityMode ||
''
)
useState<string>(currentQuery.forEmail || loggedInUser?.email)
const dependents = useMemo(
() => loggedInUser?.dependents || [],
[loggedInUser]
Expand Down Expand Up @@ -263,10 +259,10 @@ const AdvancedSettingsPanel = ({

const onMobilityProfileChange = useCallback(
(evt: QueryParamChangeEvent) => {
const value = evt.mobilityProfile
const value = evt.forEmail
setSelectedMobilityProfile(value as string)
setQueryParam({
mobilityProfile: value
forEmail: value
})
},
[setSelectedMobilityProfile, setQueryParam]
Expand Down Expand Up @@ -309,18 +305,18 @@ const AdvancedSettingsPanel = ({
label={intl.formatMessage({
id: 'components.MobilityProfile.dropdownLabel'
})}
name="mobilityProfile"
name="forEmail"
onChange={onMobilityProfileChange}
options={[
{
text: intl.formatMessage({
id: 'components.MobilityProfile.myself'
}),
value: loggedInUser.mobilityProfile?.mobilityMode || ''
value: loggedInUser?.email
},
...(loggedInUser.dependentsInfo?.map((user) => ({
text: user.name || user.email,
value: user.mobilityMode || ''
...(loggedInUser?.dependentsInfo?.map((user) => ({
text: getDependentName(user),
value: user.email
})) || [])
]}
value={selectedMobilityProfile}
Expand Down
95 changes: 95 additions & 0 deletions lib/components/user/mobility-profile/companion-selector.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { connect } from 'react-redux'
import React, { lazy, Suspense, useCallback } from 'react'

import { AppReduxState } from '../../../util/state-types'
import { CompanionInfo, User } from '../types'
import StatusBadge from '../../util/status-badge'

export interface Option {
label: string
value: CompanionInfo
}

// @ts-expect-error: No types for react-select.
const Select = lazy(() => import('react-select'))

function notNull(item: unknown) {
return !!item
}

function makeOption(companion?: CompanionInfo) {
return {
label: companion?.nickname || companion?.email,
value: companion
}
}

function isConfirmed({ status }: CompanionInfo) {
return status === 'CONFIRMED'
}

function formatOptionLabel(option: Option) {
if (!isConfirmed(option.value)) {
return (
<>
{option.label} <StatusBadge status={option.value.status} />
</>
)
} else {
return option.label
}
}

const CompanionSelector = ({
disabled,
excludedUsers = [],
loggedInUser,
multi = false,
onChange,
selectedCompanions
}: {
disabled?: boolean
excludedUsers?: (CompanionInfo | undefined)[]
loggedInUser?: User
multi?: boolean
onChange: (e: Option | Option[]) => void
selectedCompanions?: (CompanionInfo | undefined)[]
}): JSX.Element => {
const companionOptions = (loggedInUser?.relatedUsers || [])
.filter(notNull)
.filter(isConfirmed)
.map(makeOption)
const companionValues = multi
? selectedCompanions?.filter(notNull).map(makeOption)
: selectedCompanions?.[0]
? makeOption(selectedCompanions[0])
: null

const isOptionDisabled = useCallback(
(option: Option) => excludedUsers.includes(option?.value),
[excludedUsers]
)

return (
<Suspense fallback={<span>...</span>}>
<Select
formatOptionLabel={formatOptionLabel}
isClearable
isDisabled={disabled}
isMulti={multi}
isOptionDisabled={isOptionDisabled}
onChange={onChange}
options={companionOptions}
value={companionValues}
/>
</Suspense>
)
}

const mapStateToProps = (state: AppReduxState) => {
return {
loggedInUser: state.user.loggedInUser
}
}

export default connect(mapStateToProps)(CompanionSelector)
5 changes: 3 additions & 2 deletions lib/components/user/mobility-profile/companions-pane.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import React, { useCallback, useState } from 'react'
import styled from 'styled-components'

import { CompanionInfo, User } from '../types'
import { getUserWithEmail } from '../../../util/user'
import { StyledIconWrapper } from '../../util/styledIcon'
import { UnstyledButton } from '../../util/unstyled-button'
import AddEmailForm from '../common/add-email-form'
Expand Down Expand Up @@ -57,7 +58,7 @@ const CompanionRow = ({
window.confirm(
intl.formatMessage(
{ id: 'components.CompanionsPane.confirmDeleteCompanion' },
{ email: email }
{ email }
)
)
) {
Expand Down Expand Up @@ -127,7 +128,7 @@ const CompanionsPane = ({
const handleAddNewEmail = useCallback(
async ({ newEmail }, { resetForm }) => {
// Submit the new email if it is not already listed
if (!companions.find((comp) => comp.email === newEmail)) {
if (!getUserWithEmail(companions, newEmail)) {
await updateCompanions([
...companions,
{
Expand Down
114 changes: 114 additions & 0 deletions lib/components/user/mobility-profile/trip-companions-pane.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { connect } from 'react-redux'
import { FormattedMessage, IntlShape, useIntl } from 'react-intl'
import { FormikProps } from 'formik'
import React, { useCallback, useEffect } from 'react'

import * as userActions from '../../../actions/user'
import { AppReduxState } from '../../../util/state-types'
import { getDependentName } from '../../../util/user'
import { MonitoredTrip, User } from '../types'

import CompanionSelector, { Option } from './companion-selector'

type Props = FormikProps<MonitoredTrip> & {
getDependentUserInfo: (userIds: string[], intl: IntlShape) => void
isReadOnly: boolean
loggedInUser: User
}

function optionValue(option: Option | null) {
if (!option) return null
return option?.value
}

/**
* Pane for showing/setting trip companions and observers.
*/
const TripCompanions = ({
getDependentUserInfo,
isReadOnly,
loggedInUser,
setFieldValue,
values: trip
}: Props): JSX.Element => {
const handleCompanionChange = useCallback(
(option: Option | Option[] | null) => {
if (!option || 'label' in option) {
setFieldValue('companion', optionValue(option))
}
},
[setFieldValue]
)

const handleObserversChange = useCallback(
(options: Option | Option[] | null) => {
if (!options || 'length' in options) {
setFieldValue('observers', (options || []).map(optionValue))
}
},
[setFieldValue]
)

const intl = useIntl()
const dependents = loggedInUser?.dependents

useEffect(() => {
if (dependents && dependents.length > 0) {
getDependentUserInfo(dependents, intl)
}
}, [dependents, getDependentUserInfo, intl])

const { companion, observers, primary } = trip

const iAmThePrimaryTraveler =
(!primary && trip.userId === loggedInUser?.id) ||
primary?.userId === loggedInUser?.id

const primaryTraveler = iAmThePrimaryTraveler
? intl.formatMessage({ id: 'components.MobilityProfile.myself' })
: primary
? primary.name || primary.email
: getDependentName(
loggedInUser?.dependentsInfo?.find((d) => d.userId === trip.userId)
)

return (
<div>
<p>
<FormattedMessage id="components.TripCompanionsPane.primaryLabel" />
<strong>{primaryTraveler}</strong>
</p>
<p>
<FormattedMessage id="components.TripCompanionsPane.companionLabel" />
<CompanionSelector
disabled={isReadOnly || !iAmThePrimaryTraveler}
excludedUsers={observers}
onChange={handleCompanionChange}
selectedCompanions={[companion]}
/>
</p>
<p>
<FormattedMessage id="components.TripCompanionsPane.observersLabel" />
<CompanionSelector
disabled={isReadOnly}
excludedUsers={[companion]}
multi
onChange={handleObserversChange}
selectedCompanions={observers}
/>
</p>
</div>
)
}

// connect to the redux store

const mapStateToProps = (state: AppReduxState) => ({
loggedInUser: state.user.loggedInUser
})

const mapDispatchToProps = {
getDependentUserInfo: userActions.getDependentUserInfo
}

export default connect(mapStateToProps, mapDispatchToProps)(TripCompanions)
Loading

0 comments on commit 576b26b

Please sign in to comment.