Skip to content

Commit fff0b18

Browse files
committed
Extract common code for change histories
1 parent b9c8b17 commit fff0b18

21 files changed

Lines changed: 400 additions & 469 deletions

ui/src/components/stop-registry/stops/change-history/components/SectionTitle.tsx renamed to ui/src/components/common/ChangeHistory/SectionTitle.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { FC, ReactNode } from 'react';
2-
import { QuayChangeHistoryItem } from '../../../../../generated/graphql';
2+
import { BaseChangeHistoryItemDetails } from './types';
33

44
const testIds = { comment: 'ChangeHistory::SectionHeader::VersionComment' };
55

66
type SectionTitleProps = {
7-
readonly historyItem: QuayChangeHistoryItem;
7+
readonly historyItem: BaseChangeHistoryItemDetails;
88
readonly section: ReactNode;
99
};
1010

ui/src/components/common/ChangeHistory/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ export * from './DateRangeFilter';
77
export * from './EmptyCell';
88
export * from './FailedToLoadChangeHistory';
99
export * from './LoadingChangeHistory';
10+
export * from './SectionTitle';
1011
export * from './StopsList';
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { FC, ReactNode } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { GetUserNameById } from '../../../../hooks';
4+
import { SimpleButton } from '../../../../uiComponents';
5+
import {
6+
BaseChangeHistoryItemDetails,
7+
ChangeHistoryItemSectionHeaderRow,
8+
} from '../../../common/ChangeHistory';
9+
10+
type DataDiffFailedToLoadSectionProps = {
11+
readonly getUserNameById: GetUserNameById;
12+
readonly historyItem: BaseChangeHistoryItemDetails;
13+
readonly refetch: () => void;
14+
readonly sectionTitle: ReactNode;
15+
readonly headerTestId: string;
16+
readonly retryButtonTestId: string;
17+
};
18+
19+
export const DataDiffFailedToLoadSection: FC<
20+
DataDiffFailedToLoadSectionProps
21+
> = ({
22+
getUserNameById,
23+
historyItem,
24+
refetch,
25+
sectionTitle,
26+
headerTestId,
27+
retryButtonTestId,
28+
}) => {
29+
const { t } = useTranslation();
30+
31+
return (
32+
<>
33+
<ChangeHistoryItemSectionHeaderRow
34+
getUserNameById={getUserNameById}
35+
historyItem={historyItem}
36+
sectionTitle={sectionTitle}
37+
testId={headerTestId}
38+
/>
39+
<tr>
40+
<td className="p-5" colSpan={7}>
41+
{t(($) => $.changeHistory.failedToLoad)}
42+
</td>
43+
</tr>
44+
45+
<tr>
46+
<td className="p-5" colSpan={7}>
47+
<SimpleButton
48+
shape="slim"
49+
inverted
50+
onClick={refetch}
51+
testId={retryButtonTestId}
52+
>
53+
{t(($) => $.changeHistory.tryAgainButton)}
54+
</SimpleButton>
55+
</td>
56+
</tr>
57+
</>
58+
);
59+
};
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { FC, ReactNode } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { PulseLoader } from 'react-spinners';
4+
import { theme } from '../../../../generated/theme';
5+
import { GetUserNameById } from '../../../../hooks';
6+
import {
7+
BaseChangeHistoryItemDetails,
8+
ChangeHistoryItemSectionHeaderRow,
9+
} from '../../../common/ChangeHistory';
10+
11+
type DataDiffSectionLoadingProps = {
12+
readonly getUserNameById: GetUserNameById;
13+
readonly historyItem: BaseChangeHistoryItemDetails;
14+
readonly sectionTitle: ReactNode;
15+
readonly headerTestId: string;
16+
readonly contentTestId?: string;
17+
};
18+
19+
export const DataDiffSectionLoading: FC<DataDiffSectionLoadingProps> = ({
20+
getUserNameById,
21+
historyItem,
22+
sectionTitle,
23+
headerTestId,
24+
contentTestId = 'ChangeHistory::LoadingData::Content',
25+
}) => {
26+
const { t } = useTranslation();
27+
28+
return (
29+
<>
30+
<ChangeHistoryItemSectionHeaderRow
31+
getUserNameById={getUserNameById}
32+
historyItem={historyItem}
33+
sectionTitle={sectionTitle}
34+
testId={headerTestId}
35+
/>
36+
37+
<tr>
38+
<td
39+
className="p-5"
40+
colSpan={7}
41+
data-testid={contentTestId}
42+
title={t(($) => $.changeHistory.versionLoading)}
43+
>
44+
<PulseLoader color={theme.colors.brand} size={14} />
45+
</td>
46+
</tr>
47+
</>
48+
);
49+
};
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './DataDiffFailedToLoadSection';
2+
export * from './DataDiffSectionLoading';
3+
export * from './types/PreviousStopPlaceChangeHistoryItem';

ui/src/components/stop-registry/stop-areas/change-history/types/PreviousStopPlaceChangeHistoryItem.ts renamed to ui/src/components/stop-registry/components/ChangeHistory/types/PreviousStopPlaceChangeHistoryItem.ts

File renamed without changes.
Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import { TFunction } from 'i18next';
2+
import compact from 'lodash/compact';
3+
import { ReactNode } from 'react';
4+
import { InfoSpotDetailsFragment } from '../../../../../generated/graphql';
5+
import { mapZoneLabelToUiName } from '../../../../../i18n/uiNameMappings';
6+
import { getGeometryPoint } from '../../../../../utils';
7+
import {
8+
ChangedValue,
9+
EmptyCell,
10+
KeyedChangedValue,
11+
diffKeyedValues,
12+
} from '../../../../common/ChangeHistory';
13+
import { formatSizedDbItem } from '../../../stops/stop-details/info-spots/utils';
14+
import { formatPurposeForDisplay } from '../../../stops/stop-details/info-spots/utils/infoSpotPurposeUtils';
15+
import { optionalBooleanToUiText } from '../../../stops/stop-details/utils';
16+
import { normalizeZoneLabel } from '../../../types/utils';
17+
18+
type FieldValueTuple = readonly [string, string];
19+
type PosterInfo = {
20+
readonly id: string;
21+
readonly fieldValues: ReadonlyArray<FieldValueTuple>;
22+
};
23+
24+
function fv(field: string, value: string | null | undefined): FieldValueTuple {
25+
return [field, value?.trim() ?? ''];
26+
}
27+
28+
function preparePosters(
29+
t: TFunction,
30+
infoSpot: InfoSpotDetailsFragment,
31+
): Array<PosterInfo> {
32+
return compact(infoSpot.poster)
33+
.sort((a, b) => (a.id ?? '')?.localeCompare(b.id ?? ''))
34+
.map((poster) => ({
35+
id: poster.id ?? '',
36+
fieldValues: [
37+
fv(
38+
t(($) => $.stopDetails.infoSpots.posterSize),
39+
formatSizedDbItem(t, poster),
40+
),
41+
fv(
42+
t(($) => $.stopDetails.infoSpots.posterLabel),
43+
poster.label,
44+
),
45+
fv(
46+
t(($) => $.stopDetails.infoSpots.posterLines),
47+
poster.lines,
48+
),
49+
],
50+
}));
51+
}
52+
53+
function mapInfoPosters(info: ReadonlyArray<PosterInfo> | null): ReactNode {
54+
if (!info || info.length === 0) {
55+
return <EmptyCell />;
56+
}
57+
58+
return (
59+
<ul>
60+
{info.map((poster) => (
61+
<li key={poster.id}>
62+
<ul>
63+
{poster.fieldValues.map(([field, value]) => (
64+
<li key={field}>{`${field}: ${value}`}</li>
65+
))}
66+
</ul>
67+
</li>
68+
))}
69+
</ul>
70+
);
71+
}
72+
73+
export function diffInfoSpotVersions(
74+
t: TFunction,
75+
previous: InfoSpotDetailsFragment | null,
76+
current: InfoSpotDetailsFragment | null,
77+
): Array<KeyedChangedValue> {
78+
const previousPoint = getGeometryPoint(previous?.geometry);
79+
const currentPoint = getGeometryPoint(current?.geometry);
80+
81+
return compact([
82+
diffKeyedValues({
83+
key: 'Label',
84+
field: t(($) => $.stopDetails.infoSpots.label),
85+
oldValue: previous?.label,
86+
newValue: current?.label,
87+
}),
88+
diffKeyedValues({
89+
key: 'Purpose',
90+
field: t(($) => $.stopDetails.infoSpots.purpose),
91+
oldValue: previous?.purpose,
92+
newValue: current?.purpose,
93+
mapper: (v) => formatPurposeForDisplay(t, v),
94+
}),
95+
diffKeyedValues({
96+
key: 'Size',
97+
field: t(($) => $.stopDetails.infoSpots.size),
98+
oldValue: previous && formatSizedDbItem(t, previous),
99+
newValue: current && formatSizedDbItem(t, current),
100+
}),
101+
diffKeyedValues({
102+
key: 'Backlight',
103+
field: t(($) => $.stopDetails.infoSpots.backlight),
104+
oldValue: previous?.backlight,
105+
newValue: current?.backlight,
106+
mapper: (v) => optionalBooleanToUiText(t, v),
107+
}),
108+
109+
diffKeyedValues({
110+
key: 'Latitude',
111+
field: t(($) => $.stopDetails.location.latitude),
112+
oldValue: previousPoint?.latitude,
113+
newValue: currentPoint?.latitude,
114+
}),
115+
diffKeyedValues({
116+
key: 'Longitude',
117+
field: t(($) => $.stopDetails.location.longitude),
118+
oldValue: previousPoint?.longitude,
119+
newValue: currentPoint?.longitude,
120+
}),
121+
122+
diffKeyedValues({
123+
key: 'ZoneLabel',
124+
field: t(($) => $.stopDetails.infoSpots.zoneLabel),
125+
oldValue: previous?.zoneLabel,
126+
newValue: current?.zoneLabel,
127+
mapper: (v) => mapZoneLabelToUiName(t, normalizeZoneLabel(v)),
128+
}),
129+
diffKeyedValues({
130+
key: 'RailInformation',
131+
field: t(($) => $.stopDetails.infoSpots.railInformation),
132+
oldValue: previous?.railInformation,
133+
newValue: current?.railInformation,
134+
}),
135+
diffKeyedValues({
136+
key: 'Floor',
137+
field: t(($) => $.stopDetails.infoSpots.floor),
138+
oldValue: previous?.floor,
139+
newValue: current?.floor,
140+
}),
141+
diffKeyedValues({
142+
key: 'Description',
143+
field: t(($) => $.stopDetails.infoSpots.description),
144+
oldValue: previous?.description?.value,
145+
newValue: current?.description?.value,
146+
}),
147+
148+
diffKeyedValues({
149+
key: 'Posters',
150+
field: t(($) => $.changeHistory.infoSpots.posters),
151+
oldValue: previous && preparePosters(t, previous),
152+
newValue: current && preparePosters(t, current),
153+
mapper: mapInfoPosters,
154+
}),
155+
]);
156+
}
157+
158+
export function getAddedInfoSpotHeading(
159+
t: TFunction,
160+
infoSpot: InfoSpotDetailsFragment,
161+
): ChangedValue {
162+
return {
163+
key: `Added::${infoSpot.id}`,
164+
field: null,
165+
oldValue: <EmptyCell />,
166+
newValue: (
167+
<span className="font-bold">
168+
{t(($) => $.changeHistory.infoSpots.added)}
169+
</span>
170+
),
171+
};
172+
}
173+
174+
export function getUpdatedInfoSpotHeading(
175+
t: TFunction,
176+
previous: InfoSpotDetailsFragment,
177+
): ChangedValue {
178+
return {
179+
key: `Updated::${previous.id}`,
180+
field: null,
181+
oldValue: (
182+
<span className="font-bold">
183+
{t(($) => $.changeHistory.infoSpots.updated)}
184+
</span>
185+
),
186+
newValue: <EmptyCell />,
187+
};
188+
}
189+
190+
export function getRemovedInfoSpotHeading(
191+
t: TFunction,
192+
infoSpot: InfoSpotDetailsFragment,
193+
): ChangedValue {
194+
return {
195+
key: `Removed::${infoSpot.id}`,
196+
field: null,
197+
oldValue: (
198+
<span className="font-bold">
199+
{t(($) => $.changeHistory.infoSpots.removed)}
200+
</span>
201+
),
202+
newValue: <EmptyCell />,
203+
};
204+
}

0 commit comments

Comments
 (0)