Skip to content

Commit 8822410

Browse files
Merge pull request #882 from navikt/feature/searchbar-warning
Validate input for searchbar
2 parents a759ad7 + 9e266c8 commit 8822410

File tree

5 files changed

+76
-9
lines changed

5 files changed

+76
-9
lines changed

package-lock.json

Lines changed: 7 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"@navikt/arbeidsplassen-theme": "^6.0.7",
3232
"@navikt/ds-css": "^7.4.2",
3333
"@navikt/ds-react": "^7.4.2",
34+
"@navikt/fnrvalidator": "^2.1.5",
3435
"@navikt/oasis": "^3.2.2",
3536
"@neshca/cache-handler": "^1.3.1",
3637
"@sentry/nextjs": "^7.114.0",

src/app/(sok)/_components/searchBox/SearchCombobox.tsx

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ import FilterAggregations from "@/app/(sok)/_types/FilterAggregations";
1212
import { SearchLocation } from "@/app/(sok)/page";
1313
import { FilterSource } from "@/app/_common/monitoring/amplitudeHelpers";
1414
import ScreenReaderText from "./ScreenReaderText";
15+
import { containsEmail, containsValidFnrOrDnr } from "@/app/_common/utils/utils";
16+
import { ComboboxOption } from "@navikt/ds-react/esm/form/combobox/types";
1517

1618
interface SearchComboboxProps {
1719
aggregations: FilterAggregations;
@@ -20,13 +22,39 @@ interface SearchComboboxProps {
2022
function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
2123
const [showComboboxList, setShowComboboxList] = useState<boolean | undefined>(undefined);
2224
const [windowWidth, setWindowWidth] = useState<number>(0);
25+
const [errorMessage, setErrorMessage] = useState<string | null>(null);
26+
const [optionList, setOptionList] = useState<ComboboxOption[]>([]);
27+
const [filteredOptions, setFilteredOptions] = useState<ComboboxOption[]>([]);
2328
const query = useQuery();
2429

2530
const options = useMemo(() => getSearchBoxOptions(aggregations, locations), [aggregations, locations]);
2631

2732
const selectedOptions = useMemo(() => buildSelectedOptions(query.urlSearchParams), [query.urlSearchParams]);
2833

34+
const isValidFreeText = (val: string): boolean => {
35+
if (containsValidFnrOrDnr(val) || containsEmail(val)) {
36+
setErrorMessage(
37+
"Teksten du har skrevet inn kan inneholde personopplysninger. Dette er ikke tillatt av personvernhensyn. Hvis du mener dette er feil, kontakt oss på [email protected]",
38+
);
39+
return false;
40+
} else if (val.length > 100) {
41+
setErrorMessage("Søkeord kan ikke ha mer enn 100 tegn");
42+
return false;
43+
}
44+
return true;
45+
};
46+
2947
useEffect(() => {
48+
setOptionList([
49+
...options.map((o) => {
50+
const filterLabel = findLabelForFilter(o.value.split("-")[0]);
51+
return filterLabel
52+
? { label: `${o.label} ${filterLabel}`, value: o.value }
53+
: { label: o.label, value: o.value };
54+
}),
55+
...selectedOptions,
56+
]);
57+
3058
function handleResize() {
3159
setWindowWidth(window.innerWidth);
3260
}
@@ -46,20 +74,15 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
4674
}
4775
}, [selectedOptions]);
4876

49-
const optionList = options.map((o) => {
50-
const filterLabel = findLabelForFilter(o.value.split("-")[0]);
51-
return filterLabel
52-
? { label: `${o.label} ${filterLabel}`, value: o.value }
53-
: { label: o.label, value: o.value };
54-
});
55-
5677
const handleFreeTextSearchOption = (value: string, isSelected: boolean) => {
5778
if (isSelected) {
5879
query.append(QueryNames.SEARCH_STRING, value);
5980
logAmplitudeEvent("Text searched", { searchTerm: "Add" });
81+
setOptionList([...optionList, { label: value, value: value }]);
6082
} else {
6183
query.remove(QueryNames.SEARCH_STRING, value);
6284
logAmplitudeEvent("Text searched", { searchTerm: "Remove" });
85+
setOptionList(optionList.filter((option) => option.value !== value));
6386
}
6487
};
6588

@@ -144,8 +167,13 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
144167
};
145168

146169
const onToggleSelected = (option: string, isSelected: boolean, isCustomOption: boolean) => {
170+
setErrorMessage(null);
147171
if (isCustomOption) {
148-
handleFreeTextSearchOption(option, isSelected);
172+
if (isValidFreeText(option)) {
173+
handleFreeTextSearchOption(option, isSelected);
174+
} else {
175+
setShowComboboxList(false);
176+
}
149177
} else {
150178
handleFilterOption(option, isSelected);
151179
}
@@ -154,9 +182,13 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
154182
return (
155183
<>
156184
<Combobox
185+
filteredOptions={filteredOptions}
157186
onChange={(val) => {
158187
// Only show combobox list suggestion when user has started typing
159-
if (val.length > 0) {
188+
if (val.length > 0 && val.length < 100) {
189+
setFilteredOptions(
190+
optionList.filter((option) => option.label.toLowerCase().includes(val.toLowerCase())),
191+
);
160192
setShowComboboxList(undefined);
161193
} else if (selectedOptions.length > 0) {
162194
setShowComboboxList(false);
@@ -174,6 +206,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
174206
// Hide selected options in combobox below sm breakpoint
175207
shouldShowSelectedOptions={!(windowWidth < 480)}
176208
options={optionList}
209+
error={errorMessage}
177210
/>
178211
<Show below="sm">
179212
<ComboboxExternalItems

src/app/_common/utils/utils.test.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { isValidUrl } from "@/app/_common/utils/utilsts";
22
import { describe, expect, it } from "vitest";
3+
import { containsValidFnrOrDnr } from "@/app/_common/utils/utils";
34

45
describe("isValidUrl", () => {
56
it("should return true for valid URL with http protocol", () => {
@@ -52,3 +53,14 @@ describe("isValidUrl", () => {
5253
expect(isValidUrl("http://localhost:3000/stillinger/")).toBe(true);
5354
});
5455
});
56+
57+
describe("containsValidFnrOrDnr", () => {
58+
it("should contain valid id", () => {
59+
expect(containsValidFnrOrDnr("personal 13097248022 identification")).toBe(true);
60+
expect(containsValidFnrOrDnr("forgot13097248022 to use space")).toBe(true);
61+
});
62+
it("should not contain valid id", () => {
63+
expect(containsValidFnrOrDnr("personal 11111111111 identification")).toBe(false);
64+
expect(containsValidFnrOrDnr("forgot11111111111 to use space")).toBe(false);
65+
});
66+
});

src/app/_common/utils/utils.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { fnr as fnrValidator, dnr as dnrValidator } from "@navikt/fnrvalidator";
2+
13
const ISO_8601_DATE = /^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i;
24
const months = [
35
"januar",
@@ -153,4 +155,16 @@ export const SortByEnumValues = {
153155
EXPIRES: "expires",
154156
} as const;
155157

158+
export const containsValidFnrOrDnr = (input: string): boolean => {
159+
const pattern = /\d{11}/g;
160+
const matches = input.match(pattern);
161+
162+
if (matches) {
163+
return matches.some(
164+
(match) => fnrValidator(match).status === "valid" || dnrValidator(match).status === "valid",
165+
);
166+
}
167+
return false;
168+
};
169+
156170
type SortByEnumValues = (typeof SortByEnumValues)[keyof typeof SortByEnumValues];

0 commit comments

Comments
 (0)