Skip to content

Commit e3c016f

Browse files
committed
Improve Latest Changes section on Stop Details Page
Improve the change listing to better align with what is to come with the the Stop Area Latest Changes implementation. - Improved general utilities. - Dropped separate query and replaced with the normal history query: This ensures we can always access the proper previous version, even if multiple copies of the stop have been created and/or edited recently. - Fixed data fetch error retry button to actually refetch the data. - Added test ids for the data fetch loading and error states.
1 parent 6f9b99e commit e3c016f

27 files changed

Lines changed: 275 additions & 245 deletions

cypress/e2e/stop-registry/stopDetails.cy.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3087,7 +3087,7 @@ describe('Stop details', { tags: [Tag.StopRegistry] }, () => {
30873087
.shouldBeVisible()
30883088
.should('contain.text', 'Muutoshistoria');
30893089

3090-
StopDetailsPage.latestChangeHistory.getItems().should('have.length', 1);
3090+
StopDetailsPage.latestChangeHistory.getItems().should('have.length', 3);
30913091

30923092
cy.section('Make multiple changes to create history', () => {
30933093
// Change 1

cypress/pageObjects/stop-registry/StopDetailsPage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -101,8 +101,9 @@ export class StopDetailsPage {
101101
title: () => cy.getByTestId('LatestStopChangeHistoryTable::Title'),
102102
showAllLink: () =>
103103
cy.getByTestId('LatestStopChangeHistoryTable::ShowAllLink'),
104-
getItems: () => cy.getByTestId('LatestStopChangeHistoryTable::Item'),
104+
getItems: () =>
105+
cy.get('[data-testid^="LatestStopChangeHistoryTable::Item"]'),
105106
getNthItem: (index: number) =>
106-
cy.getByTestId('LatestStopChangeHistoryTable::Item').eq(index),
107+
cy.get('[data-testid^="LatestStopChangeHistoryTable::Item"]').eq(index),
107108
};
108109
}

ui/src/components/common/ChangeHistory/latest/ErrorLoadingState.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { useTranslation } from 'react-i18next';
33
import { SimpleButton } from '../../../../uiComponents';
44

55
type ErrorLoadingStateProps = {
6-
readonly onRetry?: () => void;
7-
readonly testIdPrefix?: string;
6+
readonly onRetry: () => void;
7+
readonly testIdPrefix: string;
88
};
99

1010
export const ErrorLoadingState: FC<ErrorLoadingStateProps> = ({

ui/src/components/common/ChangeHistory/latest/LatestChangeHistoryItem.tsx

Lines changed: 83 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,116 @@
1-
import { ReactNode, isValidElement } from 'react';
1+
import { FC, isValidElement } from 'react';
22
import { FaPlay } from 'react-icons/fa';
33
import { Link, To } from 'react-router';
44
import { useGetUserNames } from '../../../../hooks';
5-
import { mapUTCToDateTime } from '../../../../time';
5+
import { mapToShortDateTime } from '../../../../time';
66
import { EmptyCell } from '../EmptyCell';
7-
import { ChangedValue } from '../types';
7+
import { BaseChangeHistoryItemDetails, ChangedValue } from '../types';
8+
9+
function changeKey(change: ChangedValue): string {
10+
return change.key ?? change.field;
11+
}
12+
13+
const testIds = {
14+
change: (change: ChangedValue) =>
15+
`LatestChangeHistoryItemChange::${changeKey(change)}`,
16+
fieldName: 'LatestChangeHistoryItemChange::FieldName',
17+
oldValue: 'LatestChangeHistoryItemChange::OldValue',
18+
newValue: 'LatestChangeHistoryItemChange::NewValue',
19+
};
820

921
type ChangeSection = {
1022
readonly title: string;
1123
readonly changes: ReadonlyArray<ChangedValue>;
1224
};
1325

26+
function isEmptyCell(value: unknown): boolean {
27+
return isValidElement(value) && value.type === EmptyCell;
28+
}
29+
30+
type ChangeProps = {
31+
readonly changedValue: ChangedValue;
32+
};
33+
const LatestChangeHistoryItemChange: FC<ChangeProps> = ({ changedValue }) => {
34+
const { field, newValue, oldValue } = changedValue;
35+
const hasOldValue = oldValue && !isEmptyCell(oldValue);
36+
37+
return (
38+
<div data-testid={testIds.change(changedValue)}>
39+
<span data-testid={testIds.fieldName}>{field}</span>
40+
<span>{': '}</span>
41+
42+
{hasOldValue ? (
43+
<>
44+
<span data-testid={testIds.oldValue}>{oldValue}</span>{' '}
45+
<FaPlay className="mx-1 inline text-[8px]" />{' '}
46+
</>
47+
) : (
48+
<span data-testid={testIds.oldValue} />
49+
)}
50+
51+
<span data-testid={testIds.newValue}>{newValue}</span>
52+
</div>
53+
);
54+
};
55+
56+
type SectionProps = ChangeSection & {
57+
readonly historyItem: BaseChangeHistoryItemDetails;
58+
readonly link: To;
59+
};
60+
const LatestChangeHistoryItemSection: FC<SectionProps> = ({
61+
changes,
62+
historyItem,
63+
link,
64+
title,
65+
}) => {
66+
return (
67+
<div>
68+
<Link to={link} className="font-semibold text-brand hover:underline">
69+
{/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
70+
{historyItem.versionComment || title}
71+
</Link>
72+
{changes.map((changedValue) => (
73+
<LatestChangeHistoryItemChange
74+
key={changeKey(changedValue)}
75+
changedValue={changedValue}
76+
/>
77+
))}
78+
</div>
79+
);
80+
};
81+
1482
type LatestChangeHistoryItemProps = {
15-
readonly historyItem: {
16-
readonly changed?: string | null;
17-
readonly changedBy?: string | null;
18-
readonly versionComment?: string | null;
19-
};
83+
readonly historyItem: BaseChangeHistoryItemDetails;
2084
readonly sections: ReadonlyArray<ChangeSection>;
2185
readonly link: To;
2286
readonly testId: string;
2387
};
2488

25-
function isEmptyCell(value: unknown): boolean {
26-
return isValidElement(value) && value.type === EmptyCell;
27-
}
28-
29-
export const LatestChangeHistoryItem = ({
89+
export const LatestChangeHistoryItem: FC<LatestChangeHistoryItemProps> = ({
3090
historyItem,
3191
sections,
3292
link,
3393
testId,
34-
}: LatestChangeHistoryItemProps): ReactNode => {
94+
}) => {
3595
const { getUserNameById } = useGetUserNames();
3696

3797
const changedBy = getUserNameById(historyItem.changedBy) ?? 'HSL';
38-
const changedAt = mapUTCToDateTime(historyItem.changed);
98+
const changedAt = mapToShortDateTime(historyItem.changed);
3999

40100
if (sections.length === 0) {
41101
return null;
42102
}
43103

44104
return (
45105
<div className="mb-3 text-sm" data-testid={testId}>
46-
{sections.map((section) => (
47-
<div key={section.title}>
48-
<Link to={link} className="font-semibold text-brand hover:underline">
49-
{/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */}
50-
{historyItem.versionComment || section.title}
51-
</Link>
52-
{section.changes.map((c) => {
53-
const hasOldValue = c.oldValue && !isEmptyCell(c.oldValue);
54-
return (
55-
<span className="block" key={c.key ?? c.field}>
56-
{c.field && `${c.field}: `}
57-
{hasOldValue && (
58-
<>
59-
{c.oldValue}{' '}
60-
<FaPlay className="mx-1 inline text-[8px]" />{' '}
61-
</>
62-
)}
63-
{c.newValue}
64-
</span>
65-
);
66-
})}
67-
</div>
106+
{sections.map(({ title, changes }) => (
107+
<LatestChangeHistoryItemSection
108+
key={title}
109+
changes={changes}
110+
historyItem={historyItem}
111+
link={link}
112+
title={title}
113+
/>
68114
))}
69115
<div>
70116
{changedAt} | {changedBy}

ui/src/components/common/ChangeHistory/latest/LoadingState.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { FC } from 'react';
22
import { useTranslation } from 'react-i18next';
33

44
type LoadingStateProps = {
5-
readonly testIdPrefix?: string;
5+
readonly testIdPrefix: string;
66
};
77

88
export const LoadingState: FC<LoadingStateProps> = ({

ui/src/components/common/ChangeHistory/types/BaseChangeHistoryItem.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,5 @@ export type BaseChangeHistoryItemDetails = {
55
readonly changedBy?: string | null;
66
readonly validityEnd?: DateLike | null;
77
readonly validityStart?: DateLike | null;
8+
readonly versionComment?: string | null;
89
};
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { FC } from 'react';
2+
import { useTranslation } from 'react-i18next';
3+
import { QuayChangeHistoryItem } from '../../../../../generated/graphql';
4+
import { Path, routeDetails } from '../../../../../router/routeDetails';
5+
import { Priority } from '../../../../../types/enums';
6+
import {
7+
ErrorLoadingState,
8+
LatestChangeHistoryItem,
9+
LoadingState,
10+
} from '../../../../common/ChangeHistory/latest';
11+
import { latestStopChangeSections } from '../utils/latestStopChangeSections';
12+
import {
13+
useHistoricalStopVersion,
14+
useRefetchFailedHistoricalStopVersions,
15+
} from './HistoricalStopDataProvider';
16+
17+
const testIds = {
18+
prefix: 'LatestStopChangeHistoryTable::Item:',
19+
diff: 'LatestStopChangeHistoryTable::Item::Diffs',
20+
};
21+
22+
type LatestStopChangeDataDiffProps = {
23+
readonly historyItem: QuayChangeHistoryItem;
24+
readonly previousHistoryItem: QuayChangeHistoryItem;
25+
readonly publicCode: string;
26+
readonly priority: Priority;
27+
};
28+
29+
export const LatestStopChangeDataDiff: FC<LatestStopChangeDataDiffProps> = ({
30+
historyItem,
31+
previousHistoryItem,
32+
publicCode,
33+
priority,
34+
}) => {
35+
const { t } = useTranslation();
36+
37+
const currentCached = useHistoricalStopVersion(historyItem);
38+
const previousCached = useHistoricalStopVersion(previousHistoryItem);
39+
const refetchFailed = useRefetchFailedHistoricalStopVersions();
40+
41+
if (
42+
currentCached.status === 'fetching' ||
43+
previousCached.status === 'fetching'
44+
) {
45+
return <LoadingState testIdPrefix={testIds.prefix} />;
46+
}
47+
48+
if (
49+
currentCached.status !== 'fetched' ||
50+
previousCached.status !== 'fetched'
51+
) {
52+
return (
53+
<ErrorLoadingState
54+
onRetry={refetchFailed}
55+
testIdPrefix={testIds.prefix}
56+
/>
57+
);
58+
}
59+
60+
const sections = latestStopChangeSections(
61+
t,
62+
previousCached.value,
63+
currentCached.value,
64+
);
65+
66+
return (
67+
<LatestChangeHistoryItem
68+
historyItem={historyItem}
69+
sections={sections}
70+
link={routeDetails[Path.stopChangeHistory].getLink(publicCode, {
71+
priority: priority === Priority.Standard ? undefined : priority,
72+
})}
73+
testId={testIds.diff}
74+
/>
75+
);
76+
};

ui/src/components/stop-registry/stops/change-history/components/LatestStopChangeHistoryItem.tsx

Lines changed: 12 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,22 @@ import { Link } from 'react-router';
44
import { QuayChangeHistoryItem } from '../../../../../generated/graphql';
55
import { Path, routeDetails } from '../../../../../router/routeDetails';
66
import { Priority } from '../../../../../types/enums';
7-
import {
8-
ErrorLoadingState,
9-
LatestChangeHistoryItem,
10-
LoadingState,
11-
} from '../../../../common/ChangeHistory/latest';
12-
import { latestStopChangeSections } from '../utils/latestStopChangeSections';
13-
import { useHistoricalStopVersion } from './HistoricalStopDataProvider';
7+
import { NoEarlierVersionExists } from '../../../../common/ChangeHistory';
8+
import { LatestStopChangeDataDiff } from './LatestStopChangeDataDiff';
149
import {
1510
determineType,
1611
getHeadingText,
1712
} from './NoPreviousChangeVersionSection';
1813

1914
const testIds = {
20-
item: 'LatestStopChangeHistoryTable::Item',
15+
newItem: 'LatestStopChangeHistoryTable::Item::NewItem',
2116
};
2217

2318
type LatestStopChangeHistoryItemProps = {
2419
readonly historyItem: QuayChangeHistoryItem;
25-
readonly previousHistoryItem: QuayChangeHistoryItem | null;
20+
readonly previousHistoryItem:
21+
| QuayChangeHistoryItem
22+
| typeof NoEarlierVersionExists;
2623
readonly publicCode: string;
2724
readonly priority: Priority;
2825
};
@@ -32,53 +29,28 @@ export const LatestStopChangeHistoryItem: FC<
3229
> = ({ historyItem, previousHistoryItem, publicCode, priority }) => {
3330
const { t } = useTranslation();
3431

35-
const currentCached = useHistoricalStopVersion(historyItem);
36-
const previousCached = useHistoricalStopVersion(
37-
previousHistoryItem ?? historyItem,
38-
);
39-
4032
const link = routeDetails[Path.stopChangeHistory].getLink(publicCode, {
4133
priority: priority === Priority.Standard ? undefined : priority,
4234
});
4335

44-
if (!previousHistoryItem) {
36+
if (previousHistoryItem === NoEarlierVersionExists) {
4537
const type = determineType(historyItem);
4638
const versionText = getHeadingText(t, type);
4739
return (
48-
<div className="mb-3 text-sm font-semibold" data-testid={testIds.item}>
40+
<div className="mb-3 text-sm font-semibold" data-testid={testIds.newItem}>
4941
<Link to={link} className="text-brand hover:underline">
5042
{versionText}
5143
</Link>
5244
</div>
5345
);
5446
}
5547

56-
if (
57-
currentCached?.status === 'fetching' ||
58-
previousCached?.status === 'fetching'
59-
) {
60-
return <LoadingState />;
61-
}
62-
63-
if (
64-
currentCached?.status !== 'fetched' ||
65-
previousCached?.status !== 'fetched'
66-
) {
67-
return <ErrorLoadingState />;
68-
}
69-
70-
const sections = latestStopChangeSections(
71-
t,
72-
previousCached.value,
73-
currentCached.value,
74-
);
75-
7648
return (
77-
<LatestChangeHistoryItem
49+
<LatestStopChangeDataDiff
7850
historyItem={historyItem}
79-
sections={sections}
80-
link={link}
81-
testId={testIds.item}
51+
previousHistoryItem={previousHistoryItem}
52+
priority={priority}
53+
publicCode={publicCode}
8254
/>
8355
);
8456
};

0 commit comments

Comments
 (0)