Skip to content

Commit 3c0a97c

Browse files
committed
Add mirrored hybrid stop basic information to parent stop #35370
1 parent cf82bfa commit 3c0a97c

14 files changed

Lines changed: 330 additions & 16 deletions

File tree

ui/src/components/common/info-container/DefaultHeaderButtons.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,14 @@ export const DefaultHeaderButtons: FC<InfoContainerHeaderButtonsProps> = ({
4242
<SimpleButton
4343
shape="slim"
4444
onClick={() => setIsExpanded((expanded) => !expanded)}
45-
inverted={!isExpanded}
45+
inverted={inverted ?? !isExpanded}
4646
testId={testIds.toggle(testIdPrefix)}
4747
>
4848
{isExpanded ? (
49-
<FaChevronUp className="text-white" aria-hidden />
49+
<FaChevronUp
50+
className={inverted ? 'text-tweaked-brand' : 'text-white'}
51+
aria-hidden
52+
/>
5053
) : (
5154
<FaChevronDown className="text-tweaked-brand" aria-hidden />
5255
)}

ui/src/components/stop-registry/stops/queries/useGetMirroredQuay.ts

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,30 @@ import compact from 'lodash/compact';
22
import { useCallback } from 'react';
33
import {
44
GetStopDetailsQuery,
5+
StopRegistryStopPlaceInterface,
56
useGetStopDetailsLazyQuery,
67
} from '../../../../generated/graphql';
7-
import { EnrichedQuay, Quay, StopPlace } from '../../../../types';
8-
import { getStopPlacesFromQueryResult } from '../../../../utils';
8+
import {
9+
EnrichedQuay,
10+
EnrichedStopPlace,
11+
Quay,
12+
StopPlace,
13+
} from '../../../../types';
14+
import {
15+
getStopPlaceDetailsForEnrichment,
16+
getStopPlacesFromQueryResult,
17+
} from '../../../../utils';
918
import { mapToEnrichedQuay } from '../../utils';
1019

1120
// TODO: Currently reuses the full GetStopDetails query which fetches all fields.
1221
// Once the mirrored data requirements are finalized, create a dedicated
1322
// GetMirroredQuayDetails query with a lighter fragment to avoid overfetching.
1423

24+
export type MirroredQuayDetails = {
25+
readonly quay: EnrichedQuay;
26+
readonly stopPlace: EnrichedStopPlace;
27+
};
28+
1529
function findQuayByNetexId(
1630
data: GetStopDetailsQuery | undefined,
1731
quayNetexId: string,
@@ -33,11 +47,25 @@ function findQuayByNetexId(
3347
return null;
3448
}
3549

50+
function enrichStopPlace(stopPlace: StopPlace): EnrichedStopPlace {
51+
const transformedStopPlace = {
52+
...stopPlace,
53+
parentStopPlace: stopPlace.parentStopPlace
54+
? [stopPlace.parentStopPlace as StopRegistryStopPlaceInterface]
55+
: undefined,
56+
};
57+
58+
return {
59+
...stopPlace,
60+
...getStopPlaceDetailsForEnrichment(transformedStopPlace),
61+
};
62+
}
63+
3664
export function useGetMirroredQuay() {
3765
const [getStopDetailsLazy] = useGetStopDetailsLazyQuery();
3866

3967
return useCallback(
40-
async (quayNetexId: string): Promise<EnrichedQuay | null> => {
68+
async (quayNetexId: string): Promise<MirroredQuayDetails | null> => {
4169
const { data } = await getStopDetailsLazy({
4270
variables: {
4371
where: {
@@ -55,10 +83,18 @@ export function useGetMirroredQuay() {
5583
return null;
5684
}
5785

58-
return mapToEnrichedQuay(
86+
const enrichedQuay = mapToEnrichedQuay(
5987
result.quay,
6088
result.stopPlace.accessibilityAssessment,
6189
);
90+
if (!enrichedQuay) {
91+
return null;
92+
}
93+
94+
return {
95+
quay: enrichedQuay,
96+
stopPlace: enrichStopPlace(result.stopPlace),
97+
};
6298
},
6399
[getStopDetailsLazy],
64100
);

ui/src/components/stop-registry/stops/stop-details/StopDetailsPage.tsx

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { FC, useContext, useState } from 'react';
1+
import compact from 'lodash/compact';
2+
import { FC, useContext, useMemo, useState } from 'react';
23
import { useTranslation } from 'react-i18next';
34
import { MdWarning } from 'react-icons/md';
45
import { Link } from 'react-router';
@@ -22,6 +23,7 @@ import {
2223
} from './DetailTabSelector';
2324
import { EditStopValidityButton } from './EditStopValidityButton';
2425
import { StopExternalLinks } from './external-links/StopExternalLinks';
26+
import { MirroredQuayDetailsCard, useGetAllMirroredQuays } from './hybrid-stop';
2527
import { SheltersInfoSpotsSection } from './info-spots/SheltersInfoSpots';
2628
import { LocationDetailsSection } from './location-details';
2729
import { MaintenanceSection } from './maintenance';
@@ -56,10 +58,20 @@ export const StopDetailsPage: FC = () => {
5658
requestNavigation(() => setActiveDetailTab(nextTab));
5759

5860
const { stopDetails, loading, error } = useGetStopDetails();
61+
const { mirroredQuays } = useGetAllMirroredQuays(stopDetails?.quay);
62+
63+
const mirroredTransportModes = useMemo(
64+
() => compact(mirroredQuays.map((mq) => mq.stopPlace.transportMode)),
65+
[mirroredQuays],
66+
);
5967

6068
return (
6169
<Container testId={testIds.page}>
62-
<StopTitleRow stopDetails={stopDetails} label={label} />
70+
<StopTitleRow
71+
stopDetails={stopDetails}
72+
label={label}
73+
mirroredTransportModes={mirroredTransportModes}
74+
/>
6375
<StopHeaderSummaryRow className="my-2" stopDetails={stopDetails} />
6476
<StopDetailsVersion label={label} />
6577
<hr className="my-4" />
@@ -122,6 +134,13 @@ export const StopDetailsPage: FC = () => {
122134
role="tabpanel"
123135
>
124136
<BasicDetailsSection stop={stopDetails} />
137+
{mirroredQuays.map((mq) => (
138+
<MirroredQuayDetailsCard
139+
key={mq.quay.id}
140+
details={mq}
141+
parentStop={stopDetails}
142+
/>
143+
))}
125144
<LocationDetailsSection stop={stopDetails} />
126145
<SignageDetailsSection stop={stopDetails} />
127146
</div>

ui/src/components/stop-registry/stops/stop-details/basic-details/BasicDetailsSection.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
import { FC, useRef } from 'react';
1+
import { FC, useMemo, useRef } from 'react';
22
import { useTranslation } from 'react-i18next';
3+
import { mapTransportModeToStopTypeName } from '../../../../../i18n/uiNameMappings';
34
import { StopWithDetails } from '../../../../../types';
45
import { showSuccessToast, submitFormByRef } from '../../../../../utils';
6+
import { isMirrorParent } from '../../../../../utils/stop-registry/mirrorRelation';
57
import { InfoContainer, useInfoContainerControls } from '../../../../common';
68
import { stopInfoContainerColors } from '../stopInfoContainerColors';
79
import { StopBasicDetailsFormState } from './basic-details-form/schema';
@@ -60,11 +62,24 @@ export const BasicDetailsSection: FC<BasicDetailsSectionProps> = ({ stop }) => {
6062

6163
const defaultValues = mapStopBasicDetailsDataToFormState(stop);
6264

65+
const isHybrid = isMirrorParent(stop.quay ?? { keyValues: [] });
66+
const title = useMemo(() => {
67+
const base = t(($) => $.stopDetails.basicDetails.title);
68+
if (!isHybrid || !stop.stop_place?.transportMode) {
69+
return base;
70+
}
71+
const modeName = mapTransportModeToStopTypeName(
72+
t,
73+
stop.stop_place.transportMode,
74+
);
75+
return `${base} | ${modeName}`;
76+
}, [t, isHybrid, stop.stop_place?.transportMode]);
77+
6378
return (
6479
<InfoContainer
6580
colors={stopInfoContainerColors}
6681
controls={infoContainerControls}
67-
title={t(($) => $.stopDetails.basicDetails.title)}
82+
title={title}
6883
testIdPrefix="BasicDetailsSection"
6984
>
7085
{infoContainerControls.isInEditMode && !!defaultValues ? (
@@ -73,6 +88,7 @@ export const BasicDetailsSection: FC<BasicDetailsSectionProps> = ({ stop }) => {
7388
ref={formRef}
7489
onSubmit={onSubmit}
7590
stop={stop}
91+
isHybrid={isHybrid}
7692
onCancel={() => infoContainerControls.setIsInEditMode(false)}
7793
testIdPrefix="BasicDetailsSection"
7894
/>

ui/src/components/stop-registry/stops/stop-details/basic-details/basic-details-form/StopBasicDetailsForm.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ type StopBasicDetailsFormComponentProps = {
3030
readonly defaultValues: Partial<StopBasicDetailsFormState>;
3131
readonly onSubmit: (state: StopBasicDetailsFormState) => void;
3232
readonly stop: StopWithDetails;
33+
readonly isHybrid?: boolean;
3334
readonly onCancel: () => void;
3435
readonly testIdPrefix: string;
3536
};
@@ -38,7 +39,15 @@ const StopBasicDetailsFormComponent: ForwardRefRenderFunction<
3839
HTMLFormElement,
3940
StopBasicDetailsFormComponentProps
4041
> = (
41-
{ className, defaultValues, onSubmit, stop, onCancel, testIdPrefix },
42+
{
43+
className,
44+
defaultValues,
45+
onSubmit,
46+
stop,
47+
isHybrid,
48+
onCancel,
49+
testIdPrefix,
50+
},
4251
ref,
4352
) => {
4453
const dispatch = useDispatch();
@@ -69,6 +78,7 @@ const StopBasicDetailsFormComponent: ForwardRefRenderFunction<
6978
<HorizontalSeparator />
7079
<StopOtherDetailsFormRow
7180
onClickOpenTimingSettingsModal={openTimingPlaceModal}
81+
isTransportModeLocked={isHybrid}
7282
/>
7383
<StopTypesFormRow />
7484
<HorizontalSeparator />

ui/src/components/stop-registry/stops/stop-details/basic-details/basic-details-form/StopOtherDetailsFormRow.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,12 @@ const testIds = {
1717
};
1818
type StopOtherDetailsFormRowProps = {
1919
readonly onClickOpenTimingSettingsModal: () => void;
20+
readonly isTransportModeLocked?: boolean;
2021
};
2122

2223
export const StopOtherDetailsFormRow: FC<StopOtherDetailsFormRowProps> = ({
2324
onClickOpenTimingSettingsModal,
25+
isTransportModeLocked,
2426
}) => {
2527
const { t } = useTranslation();
2628
const { watch } = useFormContext();
@@ -43,7 +45,7 @@ export const StopOtherDetailsFormRow: FC<StopOtherDetailsFormRowProps> = ({
4345
uiNameMapper={(value) =>
4446
mapStopRegistryTransportModeTypeToUiName(t, value)
4547
}
46-
disabled={isRailReplacement}
48+
disabled={isRailReplacement || isTransportModeLocked}
4749
// eslint-disable-next-line react/jsx-props-no-spreading
4850
{...props}
4951
/>
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { FC, useMemo, useState } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import {
4+
InfrastructureNetworkDirectionEnum,
5+
StopRegistryTransportModeType,
6+
} from '../../../../../generated/graphql';
7+
import { mapTransportModeToStopTypeName } from '../../../../../i18n/uiNameMappings';
8+
import { StopWithDetails } from '../../../../../types';
9+
import { Priority } from '../../../../../types/enums';
10+
import { ConfirmationDialog, SimpleButton } from '../../../../../uiComponents';
11+
import { showSuccessToast } from '../../../../../utils';
12+
import { InfoContainer, useInfoContainerControls } from '../../../../common';
13+
import { MirroredQuayDetails } from '../../queries/useGetMirroredQuay';
14+
import { StopAreaDetailsSection } from '../basic-details/BasicDetailsStopAreaFields';
15+
import { StopDetailsSection } from '../basic-details/BasicDetailsStopFields';
16+
import { getContainerColorsByTransportMode } from '../stopInfoContainerColors';
17+
import { useRemoveMirrorRelation } from './useRemoveMirrorRelation';
18+
19+
function toStopWithDetails(details: MirroredQuayDetails): StopWithDetails {
20+
const { quay, stopPlace } = details;
21+
const coords = quay.geometry?.coordinates;
22+
return {
23+
scheduled_stop_point_id: '' as UUID,
24+
label: quay.publicCode ?? '',
25+
priority: quay.priority ?? Priority.Standard,
26+
direction: InfrastructureNetworkDirectionEnum.Forward,
27+
validity_start: null,
28+
validity_end: null,
29+
located_on_infrastructure_link_id: '' as UUID,
30+
stop_place_ref: null,
31+
measured_location: {
32+
type: 'Point' as const,
33+
coordinates: coords ?? [0, 0],
34+
},
35+
timing_place_id: (quay.timingPlaceId ?? null) as UUID | null,
36+
timing_place: null,
37+
vehicle_mode_on_scheduled_stop_point: [],
38+
stop_place: stopPlace,
39+
quay,
40+
location: {
41+
longitude: coords?.[0] ?? 0,
42+
latitude: coords?.[1] ?? 0,
43+
},
44+
};
45+
}
46+
47+
type MirroredQuayDetailsCardProps = {
48+
readonly details: MirroredQuayDetails;
49+
readonly parentStop: StopWithDetails;
50+
};
51+
52+
export const MirroredQuayDetailsCard: FC<MirroredQuayDetailsCardProps> = ({
53+
details,
54+
parentStop,
55+
}) => {
56+
const { t } = useTranslation();
57+
const [showRemoveDialog, setShowRemoveDialog] = useState(false);
58+
const { removeMirrorRelation, loading: removing } = useRemoveMirrorRelation();
59+
60+
const { transportMode } = details.stopPlace;
61+
const colors = getContainerColorsByTransportMode(transportMode);
62+
63+
const infoContainerControls = useInfoContainerControls({
64+
isEditable: false,
65+
isExpandable: true,
66+
});
67+
68+
const transportModeName = transportMode
69+
? mapTransportModeToStopTypeName(t, transportMode)
70+
: '';
71+
72+
const title = `${t(($) => $.stopDetails.basicDetails.title)} | ${transportModeName}`;
73+
74+
const pseudoStop = useMemo(() => toStopWithDetails(details), [details]);
75+
76+
const handleRemove = async () => {
77+
const success = await removeMirrorRelation({
78+
parentStop,
79+
childQuayId: details.quay.id ?? '',
80+
childStopPlaceId: details.stopPlace.id ?? '',
81+
});
82+
setShowRemoveDialog(false);
83+
if (success) {
84+
showSuccessToast(t(($) => $.stopDetails.hybrid.removeSuccess));
85+
}
86+
};
87+
88+
return (
89+
<>
90+
<InfoContainer
91+
colors={colors}
92+
controls={infoContainerControls}
93+
title={title}
94+
inverted
95+
testIdPrefix={`MirroredQuayDetails::${details.quay.id}`}
96+
>
97+
<StopAreaDetailsSection stop={pseudoStop} />
98+
<StopDetailsSection stop={pseudoStop} />
99+
<div className="mt-4 flex justify-end border-t border-light-grey pt-4">
100+
<SimpleButton
101+
inverted
102+
onClick={() => setShowRemoveDialog(true)}
103+
testId={`MirroredQuayDetails::${details.quay.id}::remove`}
104+
>
105+
{t(($) => $.stopDetails.hybrid.removeButton)}
106+
</SimpleButton>
107+
</div>
108+
</InfoContainer>
109+
<ConfirmationDialog
110+
isOpen={showRemoveDialog}
111+
onConfirm={handleRemove}
112+
onCancel={() => setShowRemoveDialog(false)}
113+
title={t(($) => $.stopDetails.hybrid.removeConfirmTitle)}
114+
description={t(($) => $.stopDetails.hybrid.removeConfirmDescription)}
115+
confirmText={t(($) => $.stopDetails.hybrid.removeConfirm)}
116+
cancelText={t(($) => $.stopDetails.hybrid.removeCancel)}
117+
isConfirming={removing}
118+
/>
119+
</>
120+
);
121+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,3 @@
11
export { MakeHybridStopModal } from './MakeHybridStopModal';
2+
export { MirroredQuayDetailsCard } from './MirroredQuayDetailsCard';
3+
export { useGetAllMirroredQuays } from './useGetAllMirroredQuays';

0 commit comments

Comments
 (0)