Skip to content

Commit d975ab4

Browse files
committed
Change route search into form
1 parent 5965681 commit d975ab4

29 files changed

+476
-568
lines changed
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import without from 'lodash/without';
2+
import { ReactElement } from 'react';
3+
import { FieldValues, Path, useController } from 'react-hook-form';
4+
import { useTranslation } from 'react-i18next';
5+
import { TranslationKey } from '../../../../i18n';
6+
import { mapPriorityToUiName } from '../../../../i18n/uiNameMappings';
7+
import { Column, Row } from '../../../../layoutComponents';
8+
import { Priority, knownPriorityValues } from '../../../../types/enums';
9+
import { InputLabel, LabeledCheckbox } from '../../../forms/common';
10+
11+
const testIds = {
12+
priorityCheckbox: (prefix: string, priority: Priority) =>
13+
`${prefix}::priority::${Priority[priority]}`,
14+
};
15+
16+
type PriorityFilterProps<FormState extends FieldValues> = {
17+
readonly fieldPath: Path<FormState>;
18+
readonly testIdPrefix: string;
19+
readonly translationPrefix: TranslationKey;
20+
readonly className?: string;
21+
readonly disabled?: boolean;
22+
};
23+
24+
export const PriorityFilter = <FormState extends FieldValues>({
25+
fieldPath,
26+
testIdPrefix,
27+
translationPrefix,
28+
className,
29+
disabled,
30+
}: PriorityFilterProps<FormState>): ReactElement => {
31+
const { t } = useTranslation();
32+
33+
const {
34+
field: { onChange, value, disabled: controllerDisabled, onBlur, ref },
35+
} = useController<FormState, typeof fieldPath>({
36+
name: fieldPath,
37+
});
38+
39+
const togglePriority = (priority: Priority) => () => {
40+
if (value.includes(priority)) {
41+
onChange(without(value, priority).toSorted());
42+
} else {
43+
onChange(value.concat(priority).toSorted());
44+
}
45+
};
46+
47+
return (
48+
<Column className={className}>
49+
<InputLabel
50+
fieldPath="priorities"
51+
translationPrefix={translationPrefix}
52+
/>
53+
<Row className="gap-2">
54+
{knownPriorityValues.map((priority) => (
55+
<LabeledCheckbox
56+
key={priority}
57+
className="h-[--input-height]"
58+
label={mapPriorityToUiName(t, priority)}
59+
onBlur={onBlur}
60+
onClick={togglePriority(priority)}
61+
disabled={!!controllerDisabled || disabled}
62+
selected={value.includes(priority)}
63+
testId={testIds.priorityCheckbox(testIdPrefix, priority)}
64+
ref={ref}
65+
/>
66+
))}
67+
</Row>
68+
</Column>
69+
);
70+
};
Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import without from 'lodash/without';
2-
import { FC } from 'react';
3-
import { useController } from 'react-hook-form';
2+
import { FC, ReactElement } from 'react';
3+
import { FieldValues, Path, useController } from 'react-hook-form';
44
import { useTranslation } from 'react-i18next';
55
import { twJoin, twMerge } from 'tailwind-merge';
6-
import { mapStopRegistryTransportModeTypeToUiName } from '../../../../../../i18n/uiNameMappings';
7-
import { Row } from '../../../../../../layoutComponents';
8-
import { JoreStopRegistryTransportModeType } from '../../../../../../types/stop-registry';
9-
import { AllOptionEnum } from '../../../../../../utils';
10-
import { StopSearchFilters } from '../../../types';
11-
import { stopSearchBarTestIds } from '../stopSearchBarTestIds';
12-
import { DisableableFilterProps } from '../Types/DisableableFilterProps';
6+
import { TranslationKey } from '../../../../i18n';
7+
import { mapStopRegistryTransportModeTypeToUiName } from '../../../../i18n/uiNameMappings';
8+
import { Row } from '../../../../layoutComponents';
9+
import { JoreStopRegistryTransportModeType } from '../../../../types/stop-registry';
10+
import { AllOptionEnum } from '../../../../utils';
1311
import s from './TransportationModeFilter.module.css';
1412

13+
const testIds = {
14+
transportationModeButton: (
15+
prefix: string,
16+
mode: JoreStopRegistryTransportModeType,
17+
) => `${prefix}::transportationMode::${mode}`,
18+
};
19+
1520
const modeIconMap: Readonly<Record<JoreStopRegistryTransportModeType, string>> =
1621
{
1722
[JoreStopRegistryTransportModeType.Bus]: 'icon-bus',
@@ -25,12 +30,14 @@ type TransportationModeButtonProps = {
2530
readonly isSelected: (mode: JoreStopRegistryTransportModeType) => boolean;
2631
readonly mode: JoreStopRegistryTransportModeType;
2732
readonly onToggle: (mode: JoreStopRegistryTransportModeType) => void;
33+
readonly testIdPrefix: string;
2834
};
2935

3036
const TransportationModeButton: FC<TransportationModeButtonProps> = ({
3137
isSelected,
3238
mode,
3339
onToggle,
40+
testIdPrefix,
3441
}) => {
3542
const { t } = useTranslation();
3643

@@ -44,7 +51,7 @@ const TransportationModeButton: FC<TransportationModeButtonProps> = ({
4451
'aria-checked:border-tweaked-brand aria-checked:bg-tweaked-brand aria-checked:text-white',
4552
modeIconMap[mode],
4653
)}
47-
data-testid={stopSearchBarTestIds.transportationModeButton(mode)}
54+
data-testid={testIds.transportationModeButton(testIdPrefix, mode)}
4855
onClick={() => onToggle(mode)}
4956
role="checkbox"
5057
type="button"
@@ -60,20 +67,32 @@ const options: ReadonlyArray<JoreStopRegistryTransportModeType> = [
6067
JoreStopRegistryTransportModeType.Metro,
6168
];
6269

63-
export const TransportationModeFilter: FC<DisableableFilterProps> = ({
70+
type TransportationModeFilterProps<FormState extends FieldValues> = {
71+
readonly fieldPath: Path<FormState>;
72+
readonly translationPrefix: TranslationKey;
73+
readonly testIdPrefix: string;
74+
readonly className?: string;
75+
readonly disabled?: boolean;
76+
};
77+
78+
export const TransportationModeFilter = <FormState extends FieldValues>({
79+
fieldPath,
80+
translationPrefix,
81+
testIdPrefix,
6482
className,
6583
disabled,
66-
}) => {
84+
}: TransportationModeFilterProps<FormState>): ReactElement => {
6785
const { t } = useTranslation();
6886

6987
const {
7088
field: { value, onBlur, onChange },
71-
} = useController<StopSearchFilters, 'transportationMode'>({
72-
name: 'transportationMode',
89+
} = useController<FormState, typeof fieldPath>({
90+
name: fieldPath,
7391
disabled,
7492
});
7593

7694
const isSelected = (mode: JoreStopRegistryTransportModeType) =>
95+
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
7796
value.includes(AllOptionEnum.All) || value.includes(mode);
7897

7998
const onToggle = (mode: JoreStopRegistryTransportModeType) => {
@@ -101,14 +120,15 @@ export const TransportationModeFilter: FC<DisableableFilterProps> = ({
101120

102121
return (
103122
<fieldset className={twMerge('flex flex-col', className)} onBlur={onBlur}>
104-
<label>{t('stopRegistrySearch.fieldLabels.transportMode')}</label>
123+
<label>{t(`${translationPrefix}.transportMode`)}</label>
105124
<Row className={twJoin('gap-1', s.noIconMargins)}>
106125
{options.map((mode) => (
107126
<TransportationModeButton
108127
key={mode}
109128
mode={mode}
110129
onToggle={onToggle}
111130
isSelected={isSelected}
131+
testIdPrefix={testIdPrefix}
112132
/>
113133
))}
114134
</Row>
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './PriorityFilter';
2+
export * from './TransportationModeFilter';
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { FC } from 'react';
22
import { useTranslation } from 'react-i18next';
33
import { twMerge } from 'tailwind-merge';
4-
import { ExpandButton } from '../../../../../../uiComponents';
4+
import { ExpandButton } from '../../../../uiComponents';
55

66
const testIds = {
77
toggleExpand: (prefix: string) => `${prefix}::chevronToggle`,
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ReactElement } from 'react';
2+
import { FieldValues, Path } from 'react-hook-form';
3+
import { useTranslation } from 'react-i18next';
4+
import { TranslationKey } from '../../../../i18n';
5+
import { Column, Row } from '../../../../layoutComponents';
6+
import {
7+
InputElement,
8+
InputLabel,
9+
ValidationErrorList,
10+
} from '../../../forms/common';
11+
12+
type SearchQueryFilterProps<FormState extends FieldValues> = {
13+
readonly fieldPath: Path<FormState>;
14+
readonly translationPrefix: TranslationKey;
15+
readonly testId: string;
16+
readonly className?: string;
17+
};
18+
19+
export const SearchQueryFilter = <FormState extends FieldValues>({
20+
className,
21+
fieldPath,
22+
translationPrefix,
23+
testId,
24+
}: SearchQueryFilterProps<FormState>): ReactElement => {
25+
const { t } = useTranslation();
26+
27+
return (
28+
<Column className={className}>
29+
<InputLabel<FormState>
30+
fieldPath={fieldPath}
31+
translationPrefix={translationPrefix}
32+
/>
33+
34+
<Row>
35+
<InputElement<FormState>
36+
className="flex-grow rounded-r-none border-r-0"
37+
fieldPath={fieldPath}
38+
id={`${translationPrefix}.query`}
39+
testId={testId}
40+
type="search"
41+
/>
42+
43+
<button
44+
className="icon-search w-[--input-height] rounded-r bg-tweaked-brand text-2xl text-white"
45+
type="submit"
46+
aria-label={t('search.search')}
47+
title={t('search.search')}
48+
/>
49+
</Row>
50+
51+
<ValidationErrorList fieldPath={fieldPath} />
52+
</Column>
53+
);
54+
};
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './ExtraFiltersToggle';
2+
export * from './SearchQueryFilter';

ui/src/components/common/search/useSearch.ts

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -61,14 +61,12 @@ export const useSearch = () => {
6161
* Pushes selected search conditions and live filters to query string.
6262
* This will trigger GraphQL request, if the searchConditions have changed.
6363
*/
64-
const handleSearch = (state?: SearchNavigationState) => {
65-
const combinedParameters = {
66-
...searchConditions,
67-
...queryParameters.filter,
68-
};
69-
64+
const handleSearch = (
65+
combinedFilters: typeof searchConditions,
66+
state?: SearchNavigationState,
67+
) => {
7068
setMultipleParametersToUrlQuery({
71-
parameters: mapObjectToQueryParameterObjects(combinedParameters),
69+
parameters: mapObjectToQueryParameterObjects(combinedFilters),
7270
pathname: `${basePath}/search`,
7371
state,
7472
});

ui/src/components/common/search/useSearchQueryParser.ts

Lines changed: 19 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,27 @@ import {
55
} from '../../../generated/graphql';
66
import { useUrlQuery } from '../../../hooks';
77
import { Priority } from '../../../types/enums';
8-
import {
9-
AllOptionEnum,
10-
DisplayedSearchResultType,
11-
SearchConditions,
12-
} from '../../../utils';
8+
import { AllOptionEnum, DisplayedSearchResultType } from '../../../utils';
139

1410
export type FilterConditions = {
1511
displayedType: DisplayedSearchResultType;
1612
};
1713

18-
/**
19-
* Search parameter object with search conditions and filter
20-
* conditions separately
21-
*/
22-
export type SearchParameters = {
23-
search: SearchConditions;
24-
filter: FilterConditions;
25-
};
26-
27-
/**
28-
* Query string object where parameters are in string format
29-
*/
30-
export type QueryStringParameters = {
31-
priorities: string;
32-
label: string;
33-
primaryVehicleMode?: string;
34-
typeOfLine?: string;
35-
displayedType: string;
14+
export type SearchConditions = {
15+
query: string;
16+
priorities: Array<Priority>;
17+
transportMode: Array<ReusableComponentsVehicleModeEnum | AllOptionEnum>;
18+
typeOfLine: RouteTypeOfLineEnum | AllOptionEnum;
19+
observationDate: DateTime;
3620
};
3721

38-
/**
39-
* Query string object where parameters are deserialized and validated
40-
* in their correct format
41-
*/
42-
export type DeserializedQueryStringParameters = {
43-
priorities: ReadonlyArray<Priority>;
44-
label: string;
45-
primaryVehicleMode?: ReusableComponentsVehicleModeEnum | AllOptionEnum;
46-
typeOfLine?: RouteTypeOfLineEnum | AllOptionEnum;
47-
displayedType: DisplayedSearchResultType;
22+
export type QueryParameters = {
23+
search: SearchConditions;
24+
filter: FilterConditions;
4825
};
4926

5027
export enum SearchQueryParameterNames {
51-
Label = 'label',
28+
Query = 'query',
5229
Priorities = 'priorities',
5330
TransportMode = 'transportMode',
5431
DisplayedType = 'displayedType',
@@ -57,29 +34,31 @@ export enum SearchQueryParameterNames {
5734
}
5835

5936
const DEFAULT_PRIORITIES = [Priority.Standard];
60-
const DEFAULT_TRANSPORT_MODE = AllOptionEnum.All;
37+
const DEFAULT_TRANSPORT_MODE = [AllOptionEnum.All];
6138
const DEFAULT_TYPE_OF_LINE = AllOptionEnum.All;
6239
const DEFAULT_DISPLAYED_DATA = DisplayedSearchResultType.Lines;
6340
const DEFAULT_LABEL = '';
6441
const DEFAULT_OBSERVATION_DATE = DateTime.now().startOf('day');
6542

66-
export const useSearchQueryParser = () => {
43+
export const useSearchQueryParser = (): QueryParameters => {
6744
const {
6845
getStringParamFromUrlQuery,
6946
getPriorityArrayFromUrlQuery,
7047
getTransportModeArrayFromUrlQuery,
7148
getEnumFromUrlQuery,
7249
getDateTimeFromUrlQuery,
7350
} = useUrlQuery();
74-
const label =
75-
getStringParamFromUrlQuery(SearchQueryParameterNames.Label) ??
51+
const query =
52+
getStringParamFromUrlQuery(SearchQueryParameterNames.Query) ??
7653
DEFAULT_LABEL;
7754

7855
const priorities =
7956
getPriorityArrayFromUrlQuery(SearchQueryParameterNames.Priorities) ??
8057
DEFAULT_PRIORITIES;
8158

82-
const transportMode =
59+
const transportMode: Array<
60+
ReusableComponentsVehicleModeEnum | AllOptionEnum
61+
> =
8362
getTransportModeArrayFromUrlQuery(
8463
SearchQueryParameterNames.TransportMode,
8564
) ?? DEFAULT_TRANSPORT_MODE;
@@ -101,7 +80,7 @@ export const useSearchQueryParser = () => {
10180

10281
return {
10382
search: {
104-
label,
83+
query,
10584
priorities,
10685
transportMode,
10786
typeOfLine,
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './mapSearchConditions';

0 commit comments

Comments
 (0)