Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trip sharing #1304

Open
wants to merge 27 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
cee60f4
feat(TripCompanionsPane): Add basic trip companions pane to trip sett…
binh-dam-ibigroup Nov 18, 2024
354d41b
refactor(user/types): Add trip companion types
binh-dam-ibigroup Nov 18, 2024
01ec9dc
feat(TripCompanionsPane): Support assigning companions
binh-dam-ibigroup Nov 19, 2024
3559218
refactor(TripCompanionPane): Unwrap props
binh-dam-ibigroup Nov 19, 2024
89bc8e0
refactor(CompanionSelector): Disable users used elsewhere
binh-dam-ibigroup Nov 19, 2024
9b2e61a
refactor(TripCompanionsPane): Disable companion selection if trip was…
binh-dam-ibigroup Nov 19, 2024
c36f597
refactor(util/user): Extract code to get user from email
binh-dam-ibigroup Nov 19, 2024
04fe8cc
feat(SavedTripScreen): Populate primary and companion traveler upon s…
binh-dam-ibigroup Nov 19, 2024
73447d9
refactor(TripCompanionPane): Relax criterion for setting observers
binh-dam-ibigroup Nov 20, 2024
10b13aa
refactor(actions/user): Usenew endpoint for retrieving trips
binh-dam-ibigroup Nov 20, 2024
ccd4d57
refactor(SavedTripScreen): Add readonly prop
binh-dam-ibigroup Nov 20, 2024
d7794a6
refactor(actions/user): Use existing endpoint for fetching trips
binh-dam-ibigroup Nov 21, 2024
0a112e7
refactor(SavedTripScreen): Check that monitoredTrip is defined for re…
binh-dam-ibigroup Nov 21, 2024
90ae3b3
Merge branch 'plan-trip-on-behalf-of' into trip-sharing
binh-dam-ibigroup Nov 21, 2024
78e06d4
improvement(StackedPanesWithSave): Hide extra/okay btns if readonly
binh-dam-ibigroup Nov 21, 2024
29a03df
refactor(Trip panes): Disable all inputs if readonly state
binh-dam-ibigroup Nov 21, 2024
5af66ee
improvement(TripCompanionsPane): Display dependent info for trip wher…
binh-dam-ibigroup Nov 21, 2024
ab722cb
Merge branch 'plan-trip-on-behalf-of' into trip-sharing
binh-dam-ibigroup Nov 21, 2024
a8cba12
refactor(TripCompanionsPane): Implement i18n
binh-dam-ibigroup Nov 21, 2024
dcbdf32
improvement(SavedTripList): Hide pause button on read-only trips
binh-dam-ibigroup Nov 21, 2024
8f33c04
refactor: Fix types
binh-dam-ibigroup Nov 22, 2024
aad9621
Merge branch 'plan-trip-on-behalf-of' into trip-sharing
binh-dam-ibigroup Nov 25, 2024
bca35c5
fix(TripCompanionsPane): Handle deleting companions.
binh-dam-ibigroup Nov 26, 2024
cc4b7a7
refactor(CompanionRefactor): Format invalid companion/observer refere…
binh-dam-ibigroup Dec 11, 2024
cf9d231
Merge branch 'dev' into trip-sharing
binh-dam-ibigroup Dec 11, 2024
4a406cd
refactor(AdvancedSettingsPanel): Remove unused ui link
binh-dam-ibigroup Dec 11, 2024
b51a077
refactor(CompanionSelector): Fix types
binh-dam-ibigroup Dec 11, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions i18n/en-US.yml
Original file line number Diff line number Diff line change
Expand Up @@ -639,6 +639,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
7 changes: 5 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 @@ -672,6 +671,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
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
3 changes: 3 additions & 0 deletions lib/components/user/common/dropdown-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ interface SelectProps {
Control?: ComponentType
children: ReactNode
defaultValue?: string | number | boolean
disabled?: boolean
label?: ReactNode
name: string
onChange?: ChangeEventHandler
Expand All @@ -19,6 +20,7 @@ export const Select = ({
Control = FormControl,
children,
defaultValue,
disabled,
label,
name,
onChange
Expand All @@ -27,6 +29,7 @@ export const Select = ({
as: Control,
componentClass: 'select',
defaultValue,
disabled,
id: name,
name,
onChange
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
120 changes: 120 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,120 @@
import { connect } from 'react-redux'
import {
FormattedMessage,
IntlShape,
useIntl,
WrappedComponentProps
} 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 = WrappedComponentProps &
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.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
Loading