Skip to content

Commit

Permalink
fix: language change breaking search forms
Browse files Browse the repository at this point in the history
  • Loading branch information
joonatank committed Dec 10, 2024
1 parent 8bee00b commit baad511
Show file tree
Hide file tree
Showing 9 changed files with 252 additions and 262 deletions.
79 changes: 40 additions & 39 deletions apps/ui/components/SortingComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,54 +1,55 @@
import React from "react";
import { Sorting } from "@/components/form";
import { useRouter } from "next/router";
import { useTranslation } from "next-i18next";
import { useSearchValues } from "@/hooks/useSearchValues";
import { type Url } from "next/dist/shared/lib/router/router";
import { useSearchParams } from "next/navigation";
import { useSearchModify } from "@/hooks/useSearchValues";

export function SortingComponent() {
const searchValues = useSearchValues();
const { t } = useTranslation();
const router = useRouter();
const SORTING_OPTIONS = [
{
label: "search:sorting.label.name",
value: "name",
},
{
label: "search:sorting.label.type",
value: "typeRank",
},
{
label: "search:sorting.label.unit",
value: "unitName",
},
] as const;

const sortingOptions = [
{
label: t("search:sorting.label.name"),
value: "name",
},
{
label: t("search:sorting.label.type"),
value: "typeRank",
},
{
label: t("search:sorting.label.unit"),
value: "unitName",
},
];
function validateSorting(
value: string | null
): (typeof SORTING_OPTIONS)[number]["value"] {
if (SORTING_OPTIONS?.some((option) => option.value === value)) {
return value as (typeof SORTING_OPTIONS)[number]["value"];
}
return "name";
}

const isOrderingAsc = searchValues.order !== "desc";
export function SortingComponent() {
const searchValues = useSearchParams();
const { handleRouteChange } = useSearchModify();
const { t } = useTranslation();

const value =
searchValues.sort != null && !Array.isArray(searchValues.sort)
? searchValues.sort
: "name";
const sortingOptions = SORTING_OPTIONS.map((option) => ({
label: t(option.label),
value: option.value,
}));

const handleRouteChange = (url: Url) => {
router.replace(url, undefined, { shallow: true, scroll: false });
};
const isOrderingAsc = searchValues.get("order") !== "desc";
const value = validateSorting(searchValues.get("sort"));

const handleSort = (sort: string) => {
const params = {
...searchValues,
sort,
};
handleRouteChange({ query: params });
const params = new URLSearchParams(searchValues);
params.set("sort", sort);
handleRouteChange(params);
};
const handleOrderChange = (order: "asc" | "desc") => {
const params = {
...searchValues,
order,
};
handleRouteChange({ query: params });
const params = new URLSearchParams(searchValues);
params.set("order", order);
handleRouteChange(params);
};

return (
Expand Down
55 changes: 30 additions & 25 deletions apps/ui/components/search/FilterTagList.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from "react";
import { useTranslation } from "next-i18next";
import { FilterTags, StyledTag, ResetButton } from "common/src/tags";
import { useSearchModify, useSearchValues } from "@/hooks/useSearchValues";
import { useSearchModify } from "@/hooks/useSearchValues";
import { type TFunction } from "i18next";
import { useSearchParams } from "next/navigation";

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

const { handleRemoveTag, handleResetTags } = useSearchModify();
const formValues = useSearchValues();
const searchValues = useSearchParams();

const formValueKeys = Object.keys(formValues);
const filterOrder = filters;
const sortedValues = [...formValueKeys].sort(
(a, b) => filterOrder.indexOf(a) - filterOrder.indexOf(b)
const possibleKeys = searchValues.keys() ?? ([] as const);

const formValueKeys: string[] = [];
for (const key of possibleKeys) {
if (!formValueKeys.includes(key)) {
formValueKeys.push(key);
}
}

const keys = [...formValueKeys].sort(
(a, b) => filters.indexOf(a) - filters.indexOf(b)
);

const filteredTags = sortedValues
const filteredTags = keys
.filter((key) => !hideList.includes(key))
.filter((key) => {
const value = formValues[key];
const value = searchValues.get(key);
if (value == null || value === "") {
return false;
}
Expand All @@ -74,41 +82,38 @@ export function FilterTagList({
return (
<FilterTags data-testid="search-form__filter--tags">
{filteredTags.map((key) => {
const value = searchValues.getAll(key);
const label = t(`searchForm:filters.${key}`, {
label: key,
value: formValues[key],
count: Number(formValues[key]),
value,
count: value.length,
});
const value = formValues[key];
// This should never happen, but for type completeness
if (value == null || value === "") {
return null;
}
// Still have the old string encoded values (key=v1,v2,v3) for backwards compatibility
// but support the better array version (key=v1&key=v2&key=v3) for new code
const isMultiSelect = multiSelectFilters.includes(key);
if (isMultiSelect) {
const values = Array.isArray(value) ? value : value.split(",");
return values.map((subValue) => (
const isOldFormat = value.length === 1 && value[0].includes(",");
const values = isOldFormat ? value[0].split(",") : value;
return values.map((val) => (
<StyledTag
id={`filter-tag__${key}-${subValue}`}
onClick={() => handleRemoveTag([subValue], key)}
onDelete={() => handleRemoveTag([subValue], key)}
key={`${key}-${subValue}`}
id={`filter-tag__${key}-${val}`}
onClick={() => handleRemoveTag(key, val)}
onDelete={() => handleRemoveTag(key, val)}
key={`${key}-${val}`}
aria-label={t(`searchForm:removeFilter`, {
value: translateTag(key, subValue),
value: translateTag(key, val),
})}
>
{translateTag(key, subValue)}
{translateTag(key, val)}
</StyledTag>
));
}
// TODO why are these different? (multi select and single select)
return (
<StyledTag
id={`filter-tag__${key}`}
onDelete={() => handleRemoveTag([key])}
// Why is there no onClick here?
onDelete={() => handleRemoveTag(key)}
onClick={() => handleRemoveTag(key)}
key={key}
aria-label={t(`searchForm:removeFilter`, {
value: label,
Expand Down
23 changes: 12 additions & 11 deletions apps/ui/components/search/SeasonalSearchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@ import { useTranslation } from "next-i18next";
import { TextInput, IconSearch } from "hds-react";
import { type SubmitHandler, useForm } from "react-hook-form";
import { participantCountOptions } from "@/modules/const";
import { useSearchModify, useSearchValues } from "@/hooks/useSearchValues";
import { useSearchModify } from "@/hooks/useSearchValues";
import { FilterTagList } from "./FilterTagList";
import { ParsedUrlQuery } from "node:querystring";
import { ControlledSelect } from "common/src/components/form/ControlledSelect";
import { ControlledMultiSelect } from "./ControlledMultiSelect";
import { BottomContainer, Filters, StyledSubmitButton } from "./styled";
Expand All @@ -14,7 +13,9 @@ import {
mapQueryParamToNumberArray,
mapSingleParamToFormValue,
} from "@/modules/search";
import SingleLabelInputGroup from "../common/SingleLabelInputGroup";
import SingleLabelInputGroup from "@/components/common/SingleLabelInputGroup";
import { type URLSearchParams } from "node:url";
import { useSearchParams } from "next/navigation";

const filterOrder = [
"applicationRound",
Expand All @@ -36,16 +37,16 @@ type FormValues = {
};

// TODO combine as much as possible with the one in single-search (move them to a common place)
function mapQueryToForm(query: ParsedUrlQuery): FormValues {
function mapQueryToForm(query: URLSearchParams): FormValues {
return {
purposes: mapQueryParamToNumberArray(query.purposes),
unit: mapQueryParamToNumberArray(query.unit),
purposes: mapQueryParamToNumberArray(query.getAll("purposes")),
unit: mapQueryParamToNumberArray(query.getAll("unit")),
reservationUnitTypes: mapQueryParamToNumberArray(
query.reservationUnitTypes
query.getAll("reservationUnitTypes")
),
minPersons: mapQueryParamToNumber(query.minPersons) ?? null,
maxPersons: mapQueryParamToNumber(query.maxPersons) ?? null,
textSearch: mapSingleParamToFormValue(query.textSearch) ?? "",
minPersons: mapQueryParamToNumber(query.getAll("minPersons")),
maxPersons: mapQueryParamToNumber(query.getAll("maxPersons")),
textSearch: mapSingleParamToFormValue(query.getAll("textSearch")) ?? "",
};
}

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

const { handleSearch } = useSearchModify();

const searchValues = useSearchValues();
const searchValues = useSearchParams();
const { control, register, handleSubmit } = useForm<FormValues>({
values: mapQueryToForm(searchValues),
});
Expand Down
71 changes: 30 additions & 41 deletions apps/ui/components/search/SingleSearchForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { ReactNode } from "react";
import React from "react";
import { useTranslation } from "next-i18next";
import { Checkbox, IconSearch, TextInput } from "hds-react";
import { type SubmitHandler, useForm, Controller } from "react-hook-form";
Expand All @@ -11,8 +11,7 @@ import { getDurationOptions, participantCountOptions } from "@/modules/const";
import { DateRangePicker } from "@/components/form";
import { FilterTagList } from "./FilterTagList";
import SingleLabelInputGroup from "@/components/common/SingleLabelInputGroup";
import { useSearchModify, useSearchValues } from "@/hooks/useSearchValues";
import { type ParsedUrlQuery } from "node:querystring";
import { useSearchModify } from "@/hooks/useSearchValues";
import { ControlledMultiSelect } from "./ControlledMultiSelect";
import { ControlledSelect } from "common/src/components/form/ControlledSelect";
import {
Expand All @@ -27,21 +26,13 @@ import {
OptionalFilters,
StyledSubmitButton,
} from "./styled";
import { useSearchParams, type ReadonlyURLSearchParams } from "next/navigation";

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

const SingleLabelRangeWrapper = styled(SingleLabelInputGroup)<{
label: string;
children: ReactNode;
}>`
& > div:not(:first-child) {
margin-top: var(--spacing-s);
}
`;

type FormValues = {
// TODO there is some confusion on the types of these
// they are actually an array of pks (number) but they are encoded as val1,val2,val3 string
Expand All @@ -60,29 +51,28 @@ type FormValues = {
textSearch: string;
};

function mapQueryToForm(query: ParsedUrlQuery): FormValues {
const dur = mapQueryParamToNumber(query.duration);
function mapQueryToForm(params: ReadonlyURLSearchParams): FormValues {
const dur = mapQueryParamToNumber(params.getAll("duration"));
const duration = dur != null && dur > 0 ? dur : null;
const showOnlyReservable =
mapSingleBooleanParamToFormValue(query.showOnlyReservable) ?? true;
mapSingleBooleanParamToFormValue(params.getAll("showOnlyReservable")) ??
true;
return {
purposes: mapQueryParamToNumberArray(query.purposes),
unit: mapQueryParamToNumberArray(query.unit),
equipments: mapQueryParamToNumberArray(query.equipments),
purposes: mapQueryParamToNumberArray(params.getAll("purposes")),
unit: mapQueryParamToNumberArray(params.getAll("unit")),
equipments: mapQueryParamToNumberArray(params.getAll("equipments")),
reservationUnitTypes: mapQueryParamToNumberArray(
query.reservationUnitTypes
params.getAll("reservationUnitTypes")
),
// ?
timeBegin: mapSingleParamToFormValue(query.timeBegin) ?? null,
timeEnd: mapSingleParamToFormValue(query.timeEnd) ?? null,
startDate: mapSingleParamToFormValue(query.startDate) ?? null,
endDate: mapSingleParamToFormValue(query.endDate) ?? null,
// number params
timeBegin: mapSingleParamToFormValue(params.getAll("timeBegin")),
timeEnd: mapSingleParamToFormValue(params.getAll("timeEnd")),
startDate: mapSingleParamToFormValue(params.getAll("startDate")),
endDate: mapSingleParamToFormValue(params.getAll("endDate")),
duration,
minPersons: mapQueryParamToNumber(query.minPersons) ?? null,
maxPersons: mapQueryParamToNumber(query.maxPersons) ?? null,
minPersons: mapQueryParamToNumber(params.getAll("minPersons")),
maxPersons: mapQueryParamToNumber(params.getAll("maxPersons")),
showOnlyReservable,
textSearch: mapSingleParamToFormValue(query.textSearch) ?? "",
textSearch: mapSingleParamToFormValue(params.getAll("textSearch")) ?? "",
};
}

Expand Down Expand Up @@ -125,18 +115,17 @@ export function SingleSearchForm({
isLoading: boolean;
}): JSX.Element | null {
const { handleSearch } = useSearchModify();

const { t } = useTranslation();
const unitTypeOptions = reservationUnitTypeOptions;
const durationOptions = getDurationOptions(t);

const searchValues = useSearchValues();
const searchValues = useSearchParams();
// TODO the users of this should be using watch
const formValues = mapQueryToForm(searchValues);
const { handleSubmit, setValue, getValues, control, register } =
useForm<FormValues>({
values: formValues,
});
const form = useForm<FormValues>({
values: formValues,
});

const { handleSubmit, setValue, getValues, control, register } = form;
const unitTypeOptions = reservationUnitTypeOptions;
const durationOptions = getDurationOptions(t);

const translateTag = (key: string, value: string): string | undefined => {
// Handle possible number / string comparison
Expand Down Expand Up @@ -195,7 +184,7 @@ export function SingleSearchForm({
options={equipmentsOptions}
label={t("searchForm:equipmentsFilter")}
/>
<SingleLabelRangeWrapper label={t("common:dateLabel")}>
<SingleLabelInputGroup label={t("common:dateLabel")}>
<DateRangePicker
startDate={fromUIDate(String(getValues("startDate")))}
endDate={fromUIDate(String(getValues("endDate")))}
Expand All @@ -220,8 +209,8 @@ export function SingleSearchForm({
endMaxDate: addYears(new Date(), 2),
}}
/>
</SingleLabelRangeWrapper>
<SingleLabelRangeWrapper label={t("common:timeLabel")}>
</SingleLabelInputGroup>
<SingleLabelInputGroup label={t("common:timeLabel")}>
<TimeRangePicker
control={control}
names={{ begin: "timeBegin", end: "timeEnd" }}
Expand All @@ -235,7 +224,7 @@ export function SingleSearchForm({
}}
clearable={{ begin: true, end: true }}
/>
</SingleLabelRangeWrapper>
</SingleLabelInputGroup>
<ControlledSelect
name="duration"
control={control}
Expand Down
Loading

0 comments on commit baad511

Please sign in to comment.