Skip to content

Commit

Permalink
Merge pull request #882 from navikt/feature/searchbar-warning
Browse files Browse the repository at this point in the history
Validate input for searchbar
  • Loading branch information
audisaudisaudis authored Feb 3, 2025
2 parents a759ad7 + 9e266c8 commit 8822410
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 9 deletions.
7 changes: 7 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"@navikt/arbeidsplassen-theme": "^6.0.7",
"@navikt/ds-css": "^7.4.2",
"@navikt/ds-react": "^7.4.2",
"@navikt/fnrvalidator": "^2.1.5",
"@navikt/oasis": "^3.2.2",
"@neshca/cache-handler": "^1.3.1",
"@sentry/nextjs": "^7.114.0",
Expand Down
51 changes: 42 additions & 9 deletions src/app/(sok)/_components/searchBox/SearchCombobox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import FilterAggregations from "@/app/(sok)/_types/FilterAggregations";
import { SearchLocation } from "@/app/(sok)/page";
import { FilterSource } from "@/app/_common/monitoring/amplitudeHelpers";
import ScreenReaderText from "./ScreenReaderText";
import { containsEmail, containsValidFnrOrDnr } from "@/app/_common/utils/utils";
import { ComboboxOption } from "@navikt/ds-react/esm/form/combobox/types";

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

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

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

const isValidFreeText = (val: string): boolean => {
if (containsValidFnrOrDnr(val) || containsEmail(val)) {
setErrorMessage(
"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]",
);
return false;
} else if (val.length > 100) {
setErrorMessage("Søkeord kan ikke ha mer enn 100 tegn");
return false;
}
return true;
};

useEffect(() => {
setOptionList([
...options.map((o) => {
const filterLabel = findLabelForFilter(o.value.split("-")[0]);
return filterLabel
? { label: `${o.label} ${filterLabel}`, value: o.value }
: { label: o.label, value: o.value };
}),
...selectedOptions,
]);

function handleResize() {
setWindowWidth(window.innerWidth);
}
Expand All @@ -46,20 +74,15 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
}
}, [selectedOptions]);

const optionList = options.map((o) => {
const filterLabel = findLabelForFilter(o.value.split("-")[0]);
return filterLabel
? { label: `${o.label} ${filterLabel}`, value: o.value }
: { label: o.label, value: o.value };
});

const handleFreeTextSearchOption = (value: string, isSelected: boolean) => {
if (isSelected) {
query.append(QueryNames.SEARCH_STRING, value);
logAmplitudeEvent("Text searched", { searchTerm: "Add" });
setOptionList([...optionList, { label: value, value: value }]);
} else {
query.remove(QueryNames.SEARCH_STRING, value);
logAmplitudeEvent("Text searched", { searchTerm: "Remove" });
setOptionList(optionList.filter((option) => option.value !== value));
}
};

Expand Down Expand Up @@ -144,8 +167,13 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
};

const onToggleSelected = (option: string, isSelected: boolean, isCustomOption: boolean) => {
setErrorMessage(null);
if (isCustomOption) {
handleFreeTextSearchOption(option, isSelected);
if (isValidFreeText(option)) {
handleFreeTextSearchOption(option, isSelected);
} else {
setShowComboboxList(false);
}
} else {
handleFilterOption(option, isSelected);
}
Expand All @@ -154,9 +182,13 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
return (
<>
<Combobox
filteredOptions={filteredOptions}
onChange={(val) => {
// Only show combobox list suggestion when user has started typing
if (val.length > 0) {
if (val.length > 0 && val.length < 100) {
setFilteredOptions(
optionList.filter((option) => option.label.toLowerCase().includes(val.toLowerCase())),
);
setShowComboboxList(undefined);
} else if (selectedOptions.length > 0) {
setShowComboboxList(false);
Expand All @@ -174,6 +206,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) {
// Hide selected options in combobox below sm breakpoint
shouldShowSelectedOptions={!(windowWidth < 480)}
options={optionList}
error={errorMessage}
/>
<Show below="sm">
<ComboboxExternalItems
Expand Down
12 changes: 12 additions & 0 deletions src/app/_common/utils/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { isValidUrl } from "@/app/_common/utils/utilsts";
import { describe, expect, it } from "vitest";
import { containsValidFnrOrDnr } from "@/app/_common/utils/utils";

describe("isValidUrl", () => {
it("should return true for valid URL with http protocol", () => {
Expand Down Expand Up @@ -52,3 +53,14 @@ describe("isValidUrl", () => {
expect(isValidUrl("http://localhost:3000/stillinger/")).toBe(true);
});
});

describe("containsValidFnrOrDnr", () => {
it("should contain valid id", () => {
expect(containsValidFnrOrDnr("personal 13097248022 identification")).toBe(true);
expect(containsValidFnrOrDnr("forgot13097248022 to use space")).toBe(true);
});
it("should not contain valid id", () => {
expect(containsValidFnrOrDnr("personal 11111111111 identification")).toBe(false);
expect(containsValidFnrOrDnr("forgot11111111111 to use space")).toBe(false);
});
});
14 changes: 14 additions & 0 deletions src/app/_common/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { fnr as fnrValidator, dnr as dnrValidator } from "@navikt/fnrvalidator";

const ISO_8601_DATE = /^\d{4}(-\d\d(-\d\d(T\d\d:\d\d(:\d\d)?(\.\d+)?(([+-]\d\d:\d\d)|Z)?)?)?)?$/i;
const months = [
"januar",
Expand Down Expand Up @@ -153,4 +155,16 @@ export const SortByEnumValues = {
EXPIRES: "expires",
} as const;

export const containsValidFnrOrDnr = (input: string): boolean => {
const pattern = /\d{11}/g;
const matches = input.match(pattern);

if (matches) {
return matches.some(
(match) => fnrValidator(match).status === "valid" || dnrValidator(match).status === "valid",
);
}
return false;
};

type SortByEnumValues = (typeof SortByEnumValues)[keyof typeof SortByEnumValues];

0 comments on commit 8822410

Please sign in to comment.