Skip to content

Commit 80abffb

Browse files
author
Joonas Hiltunen
committed
Change route search into form
1 parent 832488d commit 80abffb

33 files changed

Lines changed: 512 additions & 621 deletions
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+
};

ui/src/components/stop-registry/search/components/StopSearchBar/ExtraFilters/TransportationModeFilter.module.css renamed to ui/src/components/common/search/ExtraFilters/TransportationModeFilter.module.css

File renamed without changes.

ui/src/components/stop-registry/search/components/StopSearchBar/ExtraFilters/TransportationModeFilter.tsx renamed to ui/src/components/common/search/ExtraFilters/TransportationModeFilter.tsx

Lines changed: 47 additions & 20 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 { FieldPathByValue, FieldValues, 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,35 +67,54 @@ const options: ReadonlyArray<JoreStopRegistryTransportModeType> = [
6067
JoreStopRegistryTransportModeType.Metro,
6168
];
6269

63-
export const TransportationModeFilter: FC<DisableableFilterProps> = ({
70+
type TransportationModeFilterProps<FormState extends FieldValues> = {
71+
readonly fieldPath: FieldPathByValue<
72+
FormState,
73+
ReadonlyArray<JoreStopRegistryTransportModeType | AllOptionEnum>
74+
>;
75+
readonly translationPrefix: TranslationKey;
76+
readonly testIdPrefix: string;
77+
readonly className?: string;
78+
readonly disabled?: boolean;
79+
};
80+
81+
export const TransportationModeFilter = <FormState extends FieldValues>({
82+
fieldPath,
83+
translationPrefix,
84+
testIdPrefix,
6485
className,
6586
disabled,
66-
}) => {
87+
}: TransportationModeFilterProps<FormState>): ReactElement => {
6788
const { t } = useTranslation();
6889

6990
const {
7091
field: { value, onBlur, onChange },
71-
} = useController<StopSearchFilters, 'transportationMode'>({
72-
name: 'transportationMode',
92+
} = useController<FormState, typeof fieldPath>({
93+
name: fieldPath,
7394
disabled,
7495
});
7596

97+
// Type assertion to help TypeScript understand the correct type
98+
const typedValue = value as ReadonlyArray<
99+
JoreStopRegistryTransportModeType | AllOptionEnum
100+
>;
101+
76102
const isSelected = (mode: JoreStopRegistryTransportModeType) =>
77-
value.includes(AllOptionEnum.All) || value.includes(mode);
103+
typedValue.includes(AllOptionEnum.All) || typedValue.includes(mode);
78104

79105
const onToggle = (mode: JoreStopRegistryTransportModeType) => {
80106
// All selected → Remove clicked and add others
81-
if (value.includes(AllOptionEnum.All)) {
107+
if (typedValue.includes(AllOptionEnum.All)) {
82108
return onChange(without(options, mode));
83109
}
84110

85111
// All not selected, but clicked is selected → remove clicked
86-
if (value.includes(mode)) {
87-
return onChange(without(value, mode));
112+
if (typedValue.includes(mode)) {
113+
return onChange(without(typedValue, mode));
88114
}
89115

90116
// Clicked not selected -> Add to selection
91-
const newSelection = value.concat(mode);
117+
const newSelection = typedValue.concat(mode);
92118

93119
// If All select -> Simplify to meta option [All]
94120
if (newSelection.length === options.length) {
@@ -101,14 +127,15 @@ export const TransportationModeFilter: FC<DisableableFilterProps> = ({
101127

102128
return (
103129
<fieldset className={twMerge('flex flex-col', className)} onBlur={onBlur}>
104-
<label>{t('stopRegistrySearch.fieldLabels.transportMode')}</label>
130+
<label>{t(`${translationPrefix}.transportMode`)}</label>
105131
<Row className={twJoin('gap-1', s.noIconMargins)}>
106132
{options.map((mode) => (
107133
<TransportationModeButton
108134
key={mode}
109135
mode={mode}
110136
onToggle={onToggle}
111137
isSelected={isSelected}
138+
testIdPrefix={testIdPrefix}
112139
/>
113140
))}
114141
</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';

ui/src/components/stop-registry/search/components/StopSearchBar/BasicFilters/ExtraFiltersToggle.tsx renamed to ui/src/components/common/search/SearchBar/ExtraFiltersToggle.tsx

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: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
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+
const testIds = {
13+
searchInput: (testIdPrefix: string) => `${testIdPrefix}::searchInput`,
14+
searchButton: (testIdPrefix: string) => `${testIdPrefix}::searchButton`,
15+
};
16+
17+
type SearchQueryFilterProps<FormState extends FieldValues> = {
18+
readonly fieldPath: Path<FormState>;
19+
readonly translationPrefix: TranslationKey;
20+
readonly testIdPrefix: string;
21+
readonly className?: string;
22+
};
23+
24+
export const SearchQueryFilter = <FormState extends FieldValues>({
25+
className,
26+
fieldPath,
27+
translationPrefix,
28+
testIdPrefix,
29+
}: SearchQueryFilterProps<FormState>): ReactElement => {
30+
const { t } = useTranslation();
31+
32+
return (
33+
<Column className={className}>
34+
<InputLabel<FormState>
35+
fieldPath={fieldPath}
36+
translationPrefix={translationPrefix}
37+
/>
38+
39+
<Row>
40+
<InputElement<FormState>
41+
className="flex-grow rounded-r-none border-r-0"
42+
fieldPath={fieldPath}
43+
id={`${translationPrefix}.query`}
44+
testId={testIds.searchInput(testIdPrefix)}
45+
type="search"
46+
/>
47+
48+
<button
49+
className="icon-search w-[--input-height] rounded-r bg-tweaked-brand text-2xl text-white"
50+
type="submit"
51+
aria-label={t('search.search')}
52+
title={t('search.search')}
53+
data-testid={testIds.searchButton(testIdPrefix)}
54+
/>
55+
</Row>
56+
57+
<ValidationErrorList fieldPath={fieldPath} />
58+
</Column>
59+
);
60+
};
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/SearchInput.tsx

Lines changed: 0 additions & 36 deletions
This file was deleted.
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export * from './ExpandedSearchButtons';
2-
export * from './SearchInput';

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { useState } from 'react';
44
import { ReusableComponentsVehicleModeEnum } from '../../../generated/graphql';
55
import { mapObjectToQueryParameterObjects, useUrlQuery } from '../../../hooks';
66
import { Priority } from '../../../types/enums';
7-
import { AllOptionEnum, DisplayedSearchResultType } from '../../../utils';
7+
import {
8+
AllOptionEnum,
9+
DisplayedSearchResultType,
10+
SearchConditions,
11+
} from '../../../utils';
812
import { SearchNavigationState } from '../../routes-and-lines/search/types';
913
import { useBasePath } from './useBasePath';
1014
import { FilterConditions, useSearchQueryParser } from './useSearchQueryParser';
@@ -61,14 +65,12 @@ export const useSearch = () => {
6165
* Pushes selected search conditions and live filters to query string.
6266
* This will trigger GraphQL request, if the searchConditions have changed.
6367
*/
64-
const handleSearch = (state?: SearchNavigationState) => {
65-
const combinedParameters = {
66-
...searchConditions,
67-
...queryParameters.filter,
68-
};
69-
68+
const handleSearch = (
69+
combinedFilters: Readonly<SearchConditions>,
70+
state?: SearchNavigationState,
71+
) => {
7072
setMultipleParametersToUrlQuery({
71-
parameters: mapObjectToQueryParameterObjects(combinedParameters),
73+
parameters: mapObjectToQueryParameterObjects(combinedFilters),
7274
pathname: `${basePath}/search`,
7375
state,
7476
});

0 commit comments

Comments
 (0)