Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 2 additions & 0 deletions ui/src/components/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,7 @@ export * from './RedirectWithQuery';
export * from './RouteLineTableRow';
export * from './RouteTableRow';
export * from './TimeRangeControl';
export * from './versions';
export * from './search';
export * from './info-container';
export * from './accordionClassNames';
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { DateTime } from 'luxon';
import { Dispatch, FC, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { twMerge } from 'tailwind-merge';
import { Row } from '../../../../../layoutComponents';
import { DateRange } from '../../../../../types';
import { DateInput } from '../../../../common';
import { Row } from '../../../layoutComponents';
import { DateRange } from '../../../types';
import { DateInput } from '../DateInput';

const testIds = {
startDate: 'ScheduledVersionsContainer::DateRangeInputs::startDate',
Expand Down
12 changes: 12 additions & 0 deletions ui/src/components/common/versions/EmptyColumnHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { FC } from 'react';

type EmptyColumnHeaderProps = {
readonly className?: string;
};

export const EmptyColumnHeader: FC<EmptyColumnHeaderProps> = ({
className,
}) => (
// eslint-disable-next-line jsx-a11y/control-has-associated-label
<td className={className} />
);
19 changes: 19 additions & 0 deletions ui/src/components/common/versions/NoVersionRow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { FC } from 'react';

type NoVersionRowProps = {
readonly noVersionsText: string;
readonly colSpan: number;
};

export const NoVersionRow: FC<NoVersionRowProps> = ({
noVersionsText,
colSpan,
}) => {
return (
<tr>
<td className="border px-4 py-2 text-center font-bold" colSpan={colSpan}>
{noVersionsText}
</td>
</tr>
);
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export enum StopVersionStatus {
export enum VersionStatus {
ACTIVE = 'ACTIVE',
STANDARD = 'STANDARD',
TEMPORARY = 'TEMPORARY',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export type StopVersionTableColumn =
export type VersionTableColumn =
| 'STATUS'
| 'VALIDITY_START'
| 'VALIDITY_END'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,10 @@ import { TFunction } from 'i18next';
import { AriaAttributes, Dispatch, FC, SetStateAction } from 'react';
import { useTranslation } from 'react-i18next';
import { twJoin } from 'tailwind-merge';
import { SortOrder } from '../../../../../types';
import { ExpandButton } from '../../../../../uiComponents';
import { StopVersionTableColumn, StopVersionTableSortingInfo } from '../types';

const testIds = {
column: (type: StopVersionTableColumn) =>
`StopVersionTableHeaderSortableCell::${type.toLowerCase()}`,
sortButton: 'StopVersionTableHeaderSortableCell::sortButton',
};
import { SortOrder } from '../../../types';
import { ExpandButton } from '../../../uiComponents';
import { VersionTableSortingInfo } from './useVersionContainerControls';
import { VersionTableColumn } from './VersionTableColumn';

function getAriaSortValue(
active: boolean,
Expand All @@ -23,39 +18,45 @@ function getAriaSortValue(
return undefined;
}

function trColumnName(t: TFunction, columnType: StopVersionTableColumn) {
function trColumnName(t: TFunction, columnType: VersionTableColumn) {
switch (columnType) {
case 'CHANGED':
return t(($) => $.stopVersion.header.changed);
return t(($) => $.versions.header.changed);
case 'CHANGED_BY':
return t(($) => $.stopVersion.header.changed_by);
return t(($) => $.versions.header.changed_by);
case 'STATUS':
return t(($) => $.stopVersion.header.status);
return t(($) => $.versions.header.status);
case 'VALIDITY_END':
return t(($) => $.stopVersion.header.validity_end);
return t(($) => $.versions.header.validity_end);
case 'VALIDITY_START':
return t(($) => $.stopVersion.header.validity_start);
return t(($) => $.versions.header.validity_start);
case 'VERSION_COMMENT':
return t(($) => $.stopVersion.header.version_comment);
return t(($) => $.versions.header.version_comment);

default:
return '';
}
}

type StopVersionTableHeaderSortableCellProps = {
type VersionTableHeaderSortableCellProps = {
readonly className?: string;
readonly tdClassName?: string;
readonly columnType: StopVersionTableColumn;
readonly sortingInfo: StopVersionTableSortingInfo;
readonly setSortingInfo: Dispatch<
SetStateAction<StopVersionTableSortingInfo>
>;
readonly columnType: VersionTableColumn;
readonly sortingInfo: VersionTableSortingInfo;
readonly setSortingInfo: Dispatch<SetStateAction<VersionTableSortingInfo>>;
readonly testIdPrefix?: string;
};

export const StopVersionTableHeaderSortableCell: FC<
StopVersionTableHeaderSortableCellProps
> = ({ className, tdClassName, columnType, sortingInfo, setSortingInfo }) => {
export const VersionTableHeaderSortableCell: FC<
VersionTableHeaderSortableCellProps
> = ({
className,
tdClassName,
columnType,
sortingInfo,
setSortingInfo,
testIdPrefix = 'VersionTableHeaderSortableCell',
}) => {
const { t } = useTranslation();

const active = sortingInfo.sortBy === columnType;
Expand All @@ -81,7 +82,7 @@ export const StopVersionTableHeaderSortableCell: FC<
<td
aria-sort={getAriaSortValue(active, ascending)}
className={tdClassName}
data-testid={testIds.column(columnType)}
data-testid={`${testIdPrefix}::${columnType.toLowerCase()}`}
>
<ExpandButton
forSorting
Expand All @@ -90,7 +91,7 @@ export const StopVersionTableHeaderSortableCell: FC<
expanded={!ascending}
expandedText={trColumnName(t, columnType)}
onClick={onClick}
testId={testIds.sortButton}
testId={`${testIdPrefix}::sortButton`}
/>
</td>
);
Expand Down
11 changes: 11 additions & 0 deletions ui/src/components/common/versions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export * from './DateRangeInputs';
export * from './NoVersionRow';
export * from './useFilterVersionsByDateRange';
export * from './useVersionContainerControls';
export * from './useSortedVersions';
export * from './statusToCellClasses';
export * from './VersionStatus';
export * from './trStatus';
export * from './VersionTableColumn';
export * from './EmptyColumnHeader';
export * from './VersionTableHeaderSortableCell';
20 changes: 20 additions & 0 deletions ui/src/components/common/versions/statusToCellClasses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { VersionStatus } from './VersionStatus';

export function statusToCellClasses(status: VersionStatus): string {
switch (status) {
case VersionStatus.ACTIVE:
return 'bg-hsl-dark-green text-white';

case VersionStatus.STANDARD:
return 'bg-tweaked-brand text-white';

case VersionStatus.TEMPORARY:
return 'bg-city-bicycle-yellow';

case VersionStatus.DRAFT:
return 'bg-background';

default:
return '';
}
}
21 changes: 21 additions & 0 deletions ui/src/components/common/versions/trStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TFunction } from 'i18next';
import { VersionStatus } from './VersionStatus';

export function trStatus(t: TFunction, status: VersionStatus): string {
switch (status) {
case VersionStatus.ACTIVE:
return t(($) => $.versions.status.active);

case VersionStatus.STANDARD:
return t(($) => $.versions.status.standard);

case VersionStatus.TEMPORARY:
return t(($) => $.versions.status.temporary);

case VersionStatus.DRAFT:
return t(($) => $.versions.status.draft);

default:
return '';
}
}
29 changes: 29 additions & 0 deletions ui/src/components/common/versions/useFilterVersionsByDateRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { DateRange } from '../../../types';

type VersionWithValidity = {
readonly validity_start: DateTime;
readonly validity_end: DateTime | null;
};

export function useFilterVersionsByDateRange<T extends VersionWithValidity>(
versions: ReadonlyArray<T>,
dateRange: DateRange,
): ReadonlyArray<T> {
const from = dateRange.startDate.valueOf();
const to = dateRange.endDate.valueOf();

return useMemo(() => {
return versions.filter((version) => {
const versionFrom = version.validity_start.valueOf();
const versionTo =
version.validity_end?.valueOf() ?? Number.POSITIVE_INFINITY;

return !(
// End before range start
(versionTo < from || versionFrom > to) // Starts after range end
);
});
}, [versions, from, to]);
}
80 changes: 80 additions & 0 deletions ui/src/components/common/versions/useSortedVersions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { DateTime } from 'luxon';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { parseDate } from '../../../time';
import { SortOrder } from '../../../types';
import { trStatus } from './trStatus';
import { VersionTableSortingInfo } from './useVersionContainerControls';
import { VersionStatus } from './VersionStatus';

type VersionWithSortableFields = {
readonly status: VersionStatus;
readonly validity_start: DateTime;
readonly validity_end: DateTime | null;
readonly version_comment: string;
readonly changed: string;
readonly changedByUserName: string | null;
};

function compareDates(
dateA: DateTime | null,
dateB: DateTime | null,
nullsLast = false,
): number {
if (dateA === null && dateB === null) {
return 0;
}

if (dateA === null) {
return nullsLast ? 1 : -1;
}

if (dateB === null) {
return nullsLast ? -1 : 1;
}

return dateA.valueOf() - dateB.valueOf();
}

export function useSortedVersions<TVersion extends VersionWithSortableFields>(
sortingInfo: VersionTableSortingInfo,
versions: ReadonlyArray<TVersion>,
): ReadonlyArray<TVersion> {
const { t } = useTranslation();

const collator = useMemo(
() => new Intl.Collator(t(($) => $.languages.intlLangCode)),
[t],
);

return useMemo(() => {
const compare = (a: TVersion, b: TVersion): number => {
switch (sortingInfo.sortBy) {
case 'STATUS':
return collator.compare(trStatus(t, a.status), trStatus(t, b.status));
case 'VALIDITY_START':
return compareDates(a.validity_start, b.validity_start);
case 'VALIDITY_END':
return compareDates(a.validity_end, b.validity_end, true);
case 'VERSION_COMMENT':
return collator.compare(a.version_comment, b.version_comment);
case 'CHANGED':
return compareDates(parseDate(a.changed), parseDate(b.changed));
case 'CHANGED_BY':
return collator.compare(
a.changedByUserName ?? '',
b.changedByUserName ?? '',
);
default:
return 0;
}
};

const orderedCompare =
sortingInfo.sortOrder === SortOrder.ASCENDING
? compare
: (a: TVersion, b: TVersion) => -compare(a, b);

return versions.toSorted(orderedCompare);
}, [sortingInfo, collator, t, versions]);
}
32 changes: 32 additions & 0 deletions ui/src/components/common/versions/useVersionContainerControls.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { DateTime } from 'luxon';
import { useState } from 'react';
import { DateRange, SortOrder } from '../../../types';
import { VersionTableColumn } from './VersionTableColumn';

export type VersionTableSortingInfo = {
readonly sortBy: VersionTableColumn;
readonly sortOrder: SortOrder;
};

export function useVersionContainerControls() {
const [expanded, setExpanded] = useState<boolean>(true);

const [dateRange, setDateRange] = useState<DateRange>(() => ({
startDate: DateTime.now().minus({ month: 1 }).startOf('month'),
endDate: DateTime.now().plus({ months: 12 }).endOf('month'),
}));

const [sortingInfo, setSortingInfo] = useState<VersionTableSortingInfo>({
sortBy: 'VALIDITY_START',
sortOrder: SortOrder.ASCENDING,
});

return {
expanded,
setExpanded,
dateRange,
setDateRange,
setSortingInfo,
sortingInfo,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ import { useObservationDateQueryParam } from '../../../../hooks';
import { mapVehicleModeToUiName } from '../../../../i18n/uiNameMappings';
import { Path, routeDetails } from '../../../../router/routeDetails';
import { ExpandButton } from '../../../../uiComponents';
import { accordionClassNames } from '../../../common';
import { LabeledDetail } from '../../../stop-registry/stops/stop-details/layout';
import { accordionClassNames } from '../../../stop-registry/stops/versions/utils';
import { StopFormState } from '../types';
import { formatIsoDateString } from '../utils';

Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/map/stop-areas/StopAreaEnglishNames.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { FC, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { ExpandButton } from '../../../uiComponents';
import { accordionClassNames } from '../../common';
import { FormRow, InputField } from '../../forms/common';
import { StopAreaFormState } from '../../forms/stop-area';
import { accordionClassNames } from '../../stop-registry/stops/versions/utils';

const ID = 'StopAreaEngNameSection';
const HeaderId = 'StopAreaEngNameSection::Header';
Expand Down
2 changes: 1 addition & 1 deletion ui/src/components/map/stop-areas/StopAreaNames.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { FC, useState } from 'react';
import { useFormContext } from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { ExpandButton } from '../../../uiComponents';
import { accordionClassNames } from '../../common';
import { FormRow, InputField } from '../../forms/common';
import { StopAreaFormState } from '../../forms/stop-area';
import { accordionClassNames } from '../../stop-registry/stops/versions/utils';
import { StopAreaEnglishNames } from './StopAreaEnglishNames';

const ID = 'StopAreaNameSection';
Expand Down
Loading
Loading