Skip to content
This repository was archived by the owner on Feb 10, 2025. It is now read-only.

Commit 1832667

Browse files
committed
fix: language change breaking search forms
1 parent 56f9237 commit 1832667

9 files changed

Lines changed: 252 additions & 262 deletions

File tree

apps/ui/components/SortingComponent.tsx

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,55 @@
11
import React from "react";
22
import { Sorting } from "@/components/form";
3-
import { useRouter } from "next/router";
43
import { useTranslation } from "next-i18next";
5-
import { useSearchValues } from "@/hooks/useSearchValues";
6-
import { type Url } from "next/dist/shared/lib/router/router";
4+
import { useSearchParams } from "next/navigation";
5+
import { useSearchModify } from "@/hooks/useSearchValues";
76

8-
export function SortingComponent() {
9-
const searchValues = useSearchValues();
10-
const { t } = useTranslation();
11-
const router = useRouter();
7+
const SORTING_OPTIONS = [
8+
{
9+
label: "search:sorting.label.name",
10+
value: "name",
11+
},
12+
{
13+
label: "search:sorting.label.type",
14+
value: "typeRank",
15+
},
16+
{
17+
label: "search:sorting.label.unit",
18+
value: "unitName",
19+
},
20+
] as const;
1221

13-
const sortingOptions = [
14-
{
15-
label: t("search:sorting.label.name"),
16-
value: "name",
17-
},
18-
{
19-
label: t("search:sorting.label.type"),
20-
value: "typeRank",
21-
},
22-
{
23-
label: t("search:sorting.label.unit"),
24-
value: "unitName",
25-
},
26-
];
22+
function validateSorting(
23+
value: string | null
24+
): (typeof SORTING_OPTIONS)[number]["value"] {
25+
if (SORTING_OPTIONS?.some((option) => option.value === value)) {
26+
return value as (typeof SORTING_OPTIONS)[number]["value"];
27+
}
28+
return "name";
29+
}
2730

28-
const isOrderingAsc = searchValues.order !== "desc";
31+
export function SortingComponent() {
32+
const searchValues = useSearchParams();
33+
const { handleRouteChange } = useSearchModify();
34+
const { t } = useTranslation();
2935

30-
const value =
31-
searchValues.sort != null && !Array.isArray(searchValues.sort)
32-
? searchValues.sort
33-
: "name";
36+
const sortingOptions = SORTING_OPTIONS.map((option) => ({
37+
label: t(option.label),
38+
value: option.value,
39+
}));
3440

35-
const handleRouteChange = (url: Url) => {
36-
router.replace(url, undefined, { shallow: true, scroll: false });
37-
};
41+
const isOrderingAsc = searchValues.get("order") !== "desc";
42+
const value = validateSorting(searchValues.get("sort"));
3843

3944
const handleSort = (sort: string) => {
40-
const params = {
41-
...searchValues,
42-
sort,
43-
};
44-
handleRouteChange({ query: params });
45+
const params = new URLSearchParams(searchValues);
46+
params.set("sort", sort);
47+
handleRouteChange(params);
4548
};
4649
const handleOrderChange = (order: "asc" | "desc") => {
47-
const params = {
48-
...searchValues,
49-
order,
50-
};
51-
handleRouteChange({ query: params });
50+
const params = new URLSearchParams(searchValues);
51+
params.set("order", order);
52+
handleRouteChange(params);
5253
};
5354

5455
return (

apps/ui/components/search/FilterTagList.tsx

Lines changed: 30 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import React from "react";
22
import { useTranslation } from "next-i18next";
33
import { FilterTags, StyledTag, ResetButton } from "common/src/tags";
4-
import { useSearchModify, useSearchValues } from "@/hooks/useSearchValues";
4+
import { useSearchModify } from "@/hooks/useSearchValues";
55
import { type TFunction } from "i18next";
6+
import { useSearchParams } from "next/navigation";
67

78
type FilterTagProps = {
89
filters: readonly string[];
@@ -48,18 +49,25 @@ export function FilterTagList({
4849
const { t } = useTranslation();
4950

5051
const { handleRemoveTag, handleResetTags } = useSearchModify();
51-
const formValues = useSearchValues();
52+
const searchValues = useSearchParams();
5253

53-
const formValueKeys = Object.keys(formValues);
54-
const filterOrder = filters;
55-
const sortedValues = [...formValueKeys].sort(
56-
(a, b) => filterOrder.indexOf(a) - filterOrder.indexOf(b)
54+
const possibleKeys = searchValues.keys() ?? ([] as const);
55+
56+
const formValueKeys: string[] = [];
57+
for (const key of possibleKeys) {
58+
if (!formValueKeys.includes(key)) {
59+
formValueKeys.push(key);
60+
}
61+
}
62+
63+
const keys = [...formValueKeys].sort(
64+
(a, b) => filters.indexOf(a) - filters.indexOf(b)
5765
);
5866

59-
const filteredTags = sortedValues
67+
const filteredTags = keys
6068
.filter((key) => !hideList.includes(key))
6169
.filter((key) => {
62-
const value = formValues[key];
70+
const value = searchValues.get(key);
6371
if (value == null || value === "") {
6472
return false;
6573
}
@@ -74,41 +82,38 @@ export function FilterTagList({
7482
return (
7583
<FilterTags data-testid="search-form__filter--tags">
7684
{filteredTags.map((key) => {
85+
const value = searchValues.getAll(key);
7786
const label = t(`searchForm:filters.${key}`, {
7887
label: key,
79-
value: formValues[key],
80-
count: Number(formValues[key]),
88+
value,
89+
count: value.length,
8190
});
82-
const value = formValues[key];
83-
// This should never happen, but for type completeness
84-
if (value == null || value === "") {
85-
return null;
86-
}
8791
// Still have the old string encoded values (key=v1,v2,v3) for backwards compatibility
8892
// but support the better array version (key=v1&key=v2&key=v3) for new code
8993
const isMultiSelect = multiSelectFilters.includes(key);
9094
if (isMultiSelect) {
91-
const values = Array.isArray(value) ? value : value.split(",");
92-
return values.map((subValue) => (
95+
const isOldFormat = value.length === 1 && value[0].includes(",");
96+
const values = isOldFormat ? value[0].split(",") : value;
97+
return values.map((val) => (
9398
<StyledTag
94-
id={`filter-tag__${key}-${subValue}`}
95-
onClick={() => handleRemoveTag([subValue], key)}
96-
onDelete={() => handleRemoveTag([subValue], key)}
97-
key={`${key}-${subValue}`}
99+
id={`filter-tag__${key}-${val}`}
100+
onClick={() => handleRemoveTag(key, val)}
101+
onDelete={() => handleRemoveTag(key, val)}
102+
key={`${key}-${val}`}
98103
aria-label={t(`searchForm:removeFilter`, {
99-
value: translateTag(key, subValue),
104+
value: translateTag(key, val),
100105
})}
101106
>
102-
{translateTag(key, subValue)}
107+
{translateTag(key, val)}
103108
</StyledTag>
104109
));
105110
}
106111
// TODO why are these different? (multi select and single select)
107112
return (
108113
<StyledTag
109114
id={`filter-tag__${key}`}
110-
onDelete={() => handleRemoveTag([key])}
111-
// Why is there no onClick here?
115+
onDelete={() => handleRemoveTag(key)}
116+
onClick={() => handleRemoveTag(key)}
112117
key={key}
113118
aria-label={t(`searchForm:removeFilter`, {
114119
value: label,

apps/ui/components/search/SeasonalSearchForm.tsx

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { useTranslation } from "next-i18next";
33
import { TextInput, IconSearch } from "hds-react";
44
import { type SubmitHandler, useForm } from "react-hook-form";
55
import { participantCountOptions } from "@/modules/const";
6-
import { useSearchModify, useSearchValues } from "@/hooks/useSearchValues";
6+
import { useSearchModify } from "@/hooks/useSearchValues";
77
import { FilterTagList } from "./FilterTagList";
8-
import { ParsedUrlQuery } from "node:querystring";
98
import { ControlledSelect } from "common/src/components/form/ControlledSelect";
109
import { ControlledMultiSelect } from "./ControlledMultiSelect";
1110
import { BottomContainer, Filters, StyledSubmitButton } from "./styled";
@@ -14,7 +13,9 @@ import {
1413
mapQueryParamToNumberArray,
1514
mapSingleParamToFormValue,
1615
} from "@/modules/search";
17-
import SingleLabelInputGroup from "../common/SingleLabelInputGroup";
16+
import SingleLabelInputGroup from "@/components/common/SingleLabelInputGroup";
17+
import { type URLSearchParams } from "node:url";
18+
import { useSearchParams } from "next/navigation";
1819

1920
const filterOrder = [
2021
"applicationRound",
@@ -36,16 +37,16 @@ type FormValues = {
3637
};
3738

3839
// TODO combine as much as possible with the one in single-search (move them to a common place)
39-
function mapQueryToForm(query: ParsedUrlQuery): FormValues {
40+
function mapQueryToForm(query: URLSearchParams): FormValues {
4041
return {
41-
purposes: mapQueryParamToNumberArray(query.purposes),
42-
unit: mapQueryParamToNumberArray(query.unit),
42+
purposes: mapQueryParamToNumberArray(query.getAll("purposes")),
43+
unit: mapQueryParamToNumberArray(query.getAll("unit")),
4344
reservationUnitTypes: mapQueryParamToNumberArray(
44-
query.reservationUnitTypes
45+
query.getAll("reservationUnitTypes")
4546
),
46-
minPersons: mapQueryParamToNumber(query.minPersons) ?? null,
47-
maxPersons: mapQueryParamToNumber(query.maxPersons) ?? null,
48-
textSearch: mapSingleParamToFormValue(query.textSearch) ?? "",
47+
minPersons: mapQueryParamToNumber(query.getAll("minPersons")),
48+
maxPersons: mapQueryParamToNumber(query.getAll("maxPersons")),
49+
textSearch: mapSingleParamToFormValue(query.getAll("textSearch")) ?? "",
4950
};
5051
}
5152

@@ -65,7 +66,7 @@ export function SeasonalSearchForm({
6566

6667
const { handleSearch } = useSearchModify();
6768

68-
const searchValues = useSearchValues();
69+
const searchValues = useSearchParams();
6970
const { control, register, handleSubmit } = useForm<FormValues>({
7071
values: mapQueryToForm(searchValues),
7172
});

apps/ui/components/search/SingleSearchForm.tsx

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { ReactNode } from "react";
1+
import React from "react";
22
import { useTranslation } from "next-i18next";
33
import { Checkbox, IconSearch, TextInput } from "hds-react";
44
import { type SubmitHandler, useForm, Controller } from "react-hook-form";
@@ -11,8 +11,7 @@ import { getDurationOptions, participantCountOptions } from "@/modules/const";
1111
import { DateRangePicker } from "@/components/form";
1212
import { FilterTagList } from "./FilterTagList";
1313
import SingleLabelInputGroup from "@/components/common/SingleLabelInputGroup";
14-
import { useSearchModify, useSearchValues } from "@/hooks/useSearchValues";
15-
import { type ParsedUrlQuery } from "node:querystring";
14+
import { useSearchModify } from "@/hooks/useSearchValues";
1615
import { ControlledMultiSelect } from "./ControlledMultiSelect";
1716
import { ControlledSelect } from "common/src/components/form/ControlledSelect";
1817
import {
@@ -27,21 +26,13 @@ import {
2726
OptionalFilters,
2827
StyledSubmitButton,
2928
} from "./styled";
29+
import { useSearchParams, type ReadonlyURLSearchParams } from "next/navigation";
3030

3131
const StyledCheckBox = styled(Checkbox)`
3232
margin: 0 !important;
3333
grid-column: -2 / span 1;
3434
`;
3535

36-
const SingleLabelRangeWrapper = styled(SingleLabelInputGroup)<{
37-
label: string;
38-
children: ReactNode;
39-
}>`
40-
& > div:not(:first-child) {
41-
margin-top: var(--spacing-s);
42-
}
43-
`;
44-
4536
type FormValues = {
4637
// TODO there is some confusion on the types of these
4738
// they are actually an array of pks (number) but they are encoded as val1,val2,val3 string
@@ -60,29 +51,28 @@ type FormValues = {
6051
textSearch: string;
6152
};
6253

63-
function mapQueryToForm(query: ParsedUrlQuery): FormValues {
64-
const dur = mapQueryParamToNumber(query.duration);
54+
function mapQueryToForm(params: ReadonlyURLSearchParams): FormValues {
55+
const dur = mapQueryParamToNumber(params.getAll("duration"));
6556
const duration = dur != null && dur > 0 ? dur : null;
6657
const showOnlyReservable =
67-
mapSingleBooleanParamToFormValue(query.showOnlyReservable) ?? true;
58+
mapSingleBooleanParamToFormValue(params.getAll("showOnlyReservable")) ??
59+
true;
6860
return {
69-
purposes: mapQueryParamToNumberArray(query.purposes),
70-
unit: mapQueryParamToNumberArray(query.unit),
71-
equipments: mapQueryParamToNumberArray(query.equipments),
61+
purposes: mapQueryParamToNumberArray(params.getAll("purposes")),
62+
unit: mapQueryParamToNumberArray(params.getAll("unit")),
63+
equipments: mapQueryParamToNumberArray(params.getAll("equipments")),
7264
reservationUnitTypes: mapQueryParamToNumberArray(
73-
query.reservationUnitTypes
65+
params.getAll("reservationUnitTypes")
7466
),
75-
// ?
76-
timeBegin: mapSingleParamToFormValue(query.timeBegin) ?? null,
77-
timeEnd: mapSingleParamToFormValue(query.timeEnd) ?? null,
78-
startDate: mapSingleParamToFormValue(query.startDate) ?? null,
79-
endDate: mapSingleParamToFormValue(query.endDate) ?? null,
80-
// number params
67+
timeBegin: mapSingleParamToFormValue(params.getAll("timeBegin")),
68+
timeEnd: mapSingleParamToFormValue(params.getAll("timeEnd")),
69+
startDate: mapSingleParamToFormValue(params.getAll("startDate")),
70+
endDate: mapSingleParamToFormValue(params.getAll("endDate")),
8171
duration,
82-
minPersons: mapQueryParamToNumber(query.minPersons) ?? null,
83-
maxPersons: mapQueryParamToNumber(query.maxPersons) ?? null,
72+
minPersons: mapQueryParamToNumber(params.getAll("minPersons")),
73+
maxPersons: mapQueryParamToNumber(params.getAll("maxPersons")),
8474
showOnlyReservable,
85-
textSearch: mapSingleParamToFormValue(query.textSearch) ?? "",
75+
textSearch: mapSingleParamToFormValue(params.getAll("textSearch")) ?? "",
8676
};
8777
}
8878

@@ -125,18 +115,17 @@ export function SingleSearchForm({
125115
isLoading: boolean;
126116
}): JSX.Element | null {
127117
const { handleSearch } = useSearchModify();
128-
129118
const { t } = useTranslation();
130-
const unitTypeOptions = reservationUnitTypeOptions;
131-
const durationOptions = getDurationOptions(t);
132-
133-
const searchValues = useSearchValues();
119+
const searchValues = useSearchParams();
134120
// TODO the users of this should be using watch
135121
const formValues = mapQueryToForm(searchValues);
136-
const { handleSubmit, setValue, getValues, control, register } =
137-
useForm<FormValues>({
138-
values: formValues,
139-
});
122+
const form = useForm<FormValues>({
123+
values: formValues,
124+
});
125+
126+
const { handleSubmit, setValue, getValues, control, register } = form;
127+
const unitTypeOptions = reservationUnitTypeOptions;
128+
const durationOptions = getDurationOptions(t);
140129

141130
const translateTag = (key: string, value: string): string | undefined => {
142131
// Handle possible number / string comparison
@@ -195,7 +184,7 @@ export function SingleSearchForm({
195184
options={equipmentsOptions}
196185
label={t("searchForm:equipmentsFilter")}
197186
/>
198-
<SingleLabelRangeWrapper label={t("common:dateLabel")}>
187+
<SingleLabelInputGroup label={t("common:dateLabel")}>
199188
<DateRangePicker
200189
startDate={fromUIDate(String(getValues("startDate")))}
201190
endDate={fromUIDate(String(getValues("endDate")))}
@@ -220,8 +209,8 @@ export function SingleSearchForm({
220209
endMaxDate: addYears(new Date(), 2),
221210
}}
222211
/>
223-
</SingleLabelRangeWrapper>
224-
<SingleLabelRangeWrapper label={t("common:timeLabel")}>
212+
</SingleLabelInputGroup>
213+
<SingleLabelInputGroup label={t("common:timeLabel")}>
225214
<TimeRangePicker
226215
control={control}
227216
names={{ begin: "timeBegin", end: "timeEnd" }}
@@ -235,7 +224,7 @@ export function SingleSearchForm({
235224
}}
236225
clearable={{ begin: true, end: true }}
237226
/>
238-
</SingleLabelRangeWrapper>
227+
</SingleLabelInputGroup>
239228
<ControlledSelect
240229
name="duration"
241230
control={control}

0 commit comments

Comments
 (0)