From b93e8bd43ef504e0e2bb45487a50b03a3dbe737f Mon Sep 17 00:00:00 2001 From: Minh Ha Do Date: Mon, 20 Jan 2025 10:49:49 +0100 Subject: [PATCH 1/8] Add restriction on free text search to reduce the inclusion of personal information --- .../_components/searchBox/SearchCombobox.tsx | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index 4d6450d96..ba5279aa6 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -20,6 +20,8 @@ interface SearchComboboxProps { function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { const [showComboboxList, setShowComboboxList] = useState(undefined); const [windowWidth, setWindowWidth] = useState(0); + const [errorMessage, setErrorMessage] = useState(null); + const [canAddNewValues, setCanAddNewValues] = useState(true); const query = useQuery(); const options = useMemo(() => getSearchBoxOptions(aggregations, locations), [aggregations, locations]); @@ -151,21 +153,29 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { } }; + const clearErrorAndReEnableNewValues = () => { + setCanAddNewValues(true); + setErrorMessage(null); + }; return ( <> { // Only show combobox list suggestion when user has started typing - if (val.length > 0) { + if (val.length > 0 && val.length < 100) { + clearErrorAndReEnableNewValues(); setShowComboboxList(undefined); } else if (selectedOptions.length > 0) { setShowComboboxList(false); + } else if (val.length > 100) { + setErrorMessage("Søkeord kan ikke ha mer enn 100 tegn"); + setCanAddNewValues(false); } }} clearButton={false} enterKeyHint="done" shouldAutocomplete - allowNewValues + allowNewValues={canAddNewValues} isListOpen={showComboboxList} label="Legg til sted, yrker og andre søkeord" isMultiSelect @@ -174,6 +184,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { // Hide selected options in combobox below sm breakpoint shouldShowSelectedOptions={!(windowWidth < 480)} options={optionList} + error={errorMessage} /> Date: Mon, 20 Jan 2025 12:04:00 +0100 Subject: [PATCH 2/8] Add check for email on search --- .../(sok)/_components/searchBox/SearchCombobox.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index ba5279aa6..6b844d841 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -12,6 +12,7 @@ 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 } from "@/app/_common/utils/utils"; interface SearchComboboxProps { aggregations: FilterAggregations; @@ -153,10 +154,21 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { } }; + const personalDataErrorMessage = + "Teksten du har skrevet inn kan inneholde personopplysninger. Dette er ikke tillatt av personvernhensyn. Hvis du mener dette er feil, kontakt oss på nav.team.arbeidsplassen@nav.no"; + + const checkForEmail = (val: string) => { + if (containsEmail(val)) { + setErrorMessage(personalDataErrorMessage); + setCanAddNewValues(false); + } + }; + const clearErrorAndReEnableNewValues = () => { setCanAddNewValues(true); setErrorMessage(null); }; + return ( <> 0 && val.length < 100) { clearErrorAndReEnableNewValues(); setShowComboboxList(undefined); + checkForEmail(val); } else if (selectedOptions.length > 0) { setShowComboboxList(false); } else if (val.length > 100) { From ddda29c8084b4324fddc1c0cdae508be03999147 Mon Sep 17 00:00:00 2001 From: Minh Ha Do Date: Mon, 20 Jan 2025 13:49:09 +0100 Subject: [PATCH 3/8] Add check for fnr and dnr on search --- package-lock.json | 7 +++++++ package.json | 1 + src/app/(sok)/_components/searchBox/SearchCombobox.tsx | 10 +++++++++- src/app/_common/utils/utils.ts | 5 +++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 432c27ab2..7f924cf6b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,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", @@ -1501,6 +1502,12 @@ "integrity": "sha512-5W6dTtMGAzGa69EJWh6Y80OPLT7BbBSduM2btThduuDkH3o9ZzDg08kdebubodZgFC2W4p48Q+CwbYK1/Y80Jg==", "license": "MIT" }, + "node_modules/@navikt/fnrvalidator": { + "version": "2.1.5", + "resolved": "https://npm.pkg.github.com/download/@navikt/fnrvalidator/2.1.5/e33cbc5693b14419bbcf4d5daee2ac9eeec532e5", + "integrity": "sha512-uHzoL3ZTYwrF7OTKaihNf9xHH6S9WnGqz420HX4lEX+yu5367l+dYxI5OIRVT5MKWEDQ4DCVSAuJhoWValv6Fw==", + "license": "MIT" + }, "node_modules/@navikt/oasis": { "version": "3.4.0", "resolved": "https://npm.pkg.github.com/download/@navikt/oasis/3.4.0/7a21e6e8c8e6eca00d7e6a5c06f7e8315c0cc71a", diff --git a/package.json b/package.json index e84bcfade..2e5d72d10 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index 6b844d841..e2665b7d5 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -12,7 +12,7 @@ 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 } from "@/app/_common/utils/utils"; +import { containsEmail, isValidFnrOrDnr } from "@/app/_common/utils/utils"; interface SearchComboboxProps { aggregations: FilterAggregations; @@ -157,6 +157,13 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { const personalDataErrorMessage = "Teksten du har skrevet inn kan inneholde personopplysninger. Dette er ikke tillatt av personvernhensyn. Hvis du mener dette er feil, kontakt oss på nav.team.arbeidsplassen@nav.no"; + const checkForFnr = (val: string) => { + if (isValidFnrOrDnr(val)) { + setErrorMessage(personalDataErrorMessage); + setCanAddNewValues(false); + } + }; + const checkForEmail = (val: string) => { if (containsEmail(val)) { setErrorMessage(personalDataErrorMessage); @@ -178,6 +185,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { clearErrorAndReEnableNewValues(); setShowComboboxList(undefined); checkForEmail(val); + checkForFnr(val); } else if (selectedOptions.length > 0) { setShowComboboxList(false); } else if (val.length > 100) { diff --git a/src/app/_common/utils/utils.ts b/src/app/_common/utils/utils.ts index 1e9fa55c5..50f48c5a2 100644 --- a/src/app/_common/utils/utils.ts +++ b/src/app/_common/utils/utils.ts @@ -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", @@ -153,4 +155,7 @@ export const SortByEnumValues = { EXPIRES: "expires", } as const; +export const isValidFnrOrDnr = (input: string): boolean => + fnrValidator(input).status === "valid" || dnrValidator(input).status === "valid"; + type SortByEnumValues = (typeof SortByEnumValues)[keyof typeof SortByEnumValues]; From d668b712fdcb7b7c7eef409ee236c90b6ad15ea3 Mon Sep 17 00:00:00 2001 From: Minh Ha Do Date: Mon, 20 Jan 2025 13:54:47 +0100 Subject: [PATCH 4/8] Refactor input validation, add check input containing id Refactor input validation --- .../_components/searchBox/SearchCombobox.tsx | 31 +++++++------------ src/app/_common/utils/utils.test.ts | 12 +++++++ src/app/_common/utils/utils.ts | 13 ++++++-- 3 files changed, 34 insertions(+), 22 deletions(-) diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index e2665b7d5..845b9452f 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -12,7 +12,7 @@ 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, isValidFnrOrDnr } from "@/app/_common/utils/utils"; +import { containsEmail, containsValidFnrOrDnr } from "@/app/_common/utils/utils"; interface SearchComboboxProps { aggregations: FilterAggregations; @@ -154,38 +154,29 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { } }; - const personalDataErrorMessage = - "Teksten du har skrevet inn kan inneholde personopplysninger. Dette er ikke tillatt av personvernhensyn. Hvis du mener dette er feil, kontakt oss på nav.team.arbeidsplassen@nav.no"; - - const checkForFnr = (val: string) => { - if (isValidFnrOrDnr(val)) { - setErrorMessage(personalDataErrorMessage); - setCanAddNewValues(false); - } + const resetErrorAndReEnableNewValues = () => { + setCanAddNewValues(true); + setErrorMessage(null); }; - const checkForEmail = (val: string) => { - if (containsEmail(val)) { - setErrorMessage(personalDataErrorMessage); + const validateInput = (val: string) => { + 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å nav.team.arbeidsplassen@nav.no", + ); setCanAddNewValues(false); } }; - const clearErrorAndReEnableNewValues = () => { - setCanAddNewValues(true); - setErrorMessage(null); - }; - return ( <> { // Only show combobox list suggestion when user has started typing if (val.length > 0 && val.length < 100) { - clearErrorAndReEnableNewValues(); + resetErrorAndReEnableNewValues(); setShowComboboxList(undefined); - checkForEmail(val); - checkForFnr(val); + validateInput(val); } else if (selectedOptions.length > 0) { setShowComboboxList(false); } else if (val.length > 100) { diff --git a/src/app/_common/utils/utils.test.ts b/src/app/_common/utils/utils.test.ts index 3c413e5bd..3e6b3cc0d 100644 --- a/src/app/_common/utils/utils.test.ts +++ b/src/app/_common/utils/utils.test.ts @@ -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", () => { @@ -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); + }); +}); diff --git a/src/app/_common/utils/utils.ts b/src/app/_common/utils/utils.ts index 50f48c5a2..d64d43edd 100644 --- a/src/app/_common/utils/utils.ts +++ b/src/app/_common/utils/utils.ts @@ -155,7 +155,16 @@ export const SortByEnumValues = { EXPIRES: "expires", } as const; -export const isValidFnrOrDnr = (input: string): boolean => - fnrValidator(input).status === "valid" || dnrValidator(input).status === "valid"; +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]; From 586c29c6a0d619ec774da95e330819f0d0cdfed1 Mon Sep 17 00:00:00 2001 From: Minh Ha Do Date: Wed, 22 Jan 2025 14:19:13 +0100 Subject: [PATCH 5/8] wip Refactor validation to activate after submit --- .../_components/searchBox/SearchCombobox.tsx | 49 ++++++++++--------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index 845b9452f..b9e544436 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -56,13 +56,31 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { : { label: o.label, value: o.value }; }); + 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å nav.team.arbeidsplassen@nav.no", + ); + return false; + } else if (val.length > 100) { + setErrorMessage("Søkeord kan ikke ha mer enn 100 tegn"); + return false; + } + return true; + }; + const handleFreeTextSearchOption = (value: string, isSelected: boolean) => { - if (isSelected) { - query.append(QueryNames.SEARCH_STRING, value); - logAmplitudeEvent("Text searched", { searchTerm: "Add" }); + if (isValidFreeText(value)) { + if (isSelected) { + query.append(QueryNames.SEARCH_STRING, value); + logAmplitudeEvent("Text searched", { searchTerm: "Add" }); + } else { + query.remove(QueryNames.SEARCH_STRING, value); + logAmplitudeEvent("Text searched", { searchTerm: "Remove" }); + } } else { - query.remove(QueryNames.SEARCH_STRING, value); - logAmplitudeEvent("Text searched", { searchTerm: "Remove" }); + setCanAddNewValues(false); + setShowComboboxList(false); } }; @@ -147,6 +165,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { }; const onToggleSelected = (option: string, isSelected: boolean, isCustomOption: boolean) => { + setErrorMessage(null); if (isCustomOption) { handleFreeTextSearchOption(option, isSelected); } else { @@ -154,19 +173,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { } }; - const resetErrorAndReEnableNewValues = () => { - setCanAddNewValues(true); - setErrorMessage(null); - }; - - const validateInput = (val: string) => { - 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å nav.team.arbeidsplassen@nav.no", - ); - setCanAddNewValues(false); - } - }; + // TODO: remove invalid options for optionList? return ( <> @@ -174,14 +181,10 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { onChange={(val) => { // Only show combobox list suggestion when user has started typing if (val.length > 0 && val.length < 100) { - resetErrorAndReEnableNewValues(); + setCanAddNewValues(true); setShowComboboxList(undefined); - validateInput(val); } else if (selectedOptions.length > 0) { setShowComboboxList(false); - } else if (val.length > 100) { - setErrorMessage("Søkeord kan ikke ha mer enn 100 tegn"); - setCanAddNewValues(false); } }} clearButton={false} From f13afafbeac3b142498f5aa5983016cc10fce5ec Mon Sep 17 00:00:00 2001 From: Minh Ha Do Date: Wed, 22 Jan 2025 15:10:02 +0100 Subject: [PATCH 6/8] wip Refactor --- .../_components/searchBox/SearchCombobox.tsx | 24 +++++++++---------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index b9e544436..0fdfc0659 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -70,17 +70,12 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { }; const handleFreeTextSearchOption = (value: string, isSelected: boolean) => { - if (isValidFreeText(value)) { - if (isSelected) { - query.append(QueryNames.SEARCH_STRING, value); - logAmplitudeEvent("Text searched", { searchTerm: "Add" }); - } else { - query.remove(QueryNames.SEARCH_STRING, value); - logAmplitudeEvent("Text searched", { searchTerm: "Remove" }); - } + if (isSelected) { + query.append(QueryNames.SEARCH_STRING, value); + logAmplitudeEvent("Text searched", { searchTerm: "Add" }); } else { - setCanAddNewValues(false); - setShowComboboxList(false); + query.remove(QueryNames.SEARCH_STRING, value); + logAmplitudeEvent("Text searched", { searchTerm: "Remove" }); } }; @@ -167,14 +162,17 @@ 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 { + setCanAddNewValues(false); + setShowComboboxList(false); + } } else { handleFilterOption(option, isSelected); } }; - // TODO: remove invalid options for optionList? - return ( <> Date: Wed, 22 Jan 2025 15:26:15 +0100 Subject: [PATCH 7/8] wip filter out invalid free text added --- .../_components/searchBox/SearchCombobox.tsx | 41 +++++++++++++------ 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index 0fdfc0659..602f73c43 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -29,6 +29,33 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { 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å nav.team.arbeidsplassen@nav.no", + ); + return false; + } else if (val.length > 100) { + setErrorMessage("Søkeord kan ikke ha mer enn 100 tegn"); + return false; + } + return true; + }; + + // TODO: remove invalid options from optionList + const filteredOptions = useMemo( + () => + options + .filter((option) => isValidFreeText(option.value)) + .map((o) => { + const filterLabel = findLabelForFilter(o.value.split("-")[0]); + return filterLabel + ? { label: `${o.label} ${filterLabel}`, value: o.value } + : { label: o.label, value: o.value }; + }), + [], + ); + useEffect(() => { function handleResize() { setWindowWidth(window.innerWidth); @@ -56,19 +83,6 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { : { label: o.label, value: o.value }; }); - 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å nav.team.arbeidsplassen@nav.no", - ); - return false; - } else if (val.length > 100) { - setErrorMessage("Søkeord kan ikke ha mer enn 100 tegn"); - return false; - } - return true; - }; - const handleFreeTextSearchOption = (value: string, isSelected: boolean) => { if (isSelected) { query.append(QueryNames.SEARCH_STRING, value); @@ -176,6 +190,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { return ( <> { // Only show combobox list suggestion when user has started typing if (val.length > 0 && val.length < 100) { From 9e266c867781d617a20eb4577f81c7d8199f5bd6 Mon Sep 17 00:00:00 2001 From: Audun Skjelvan Date: Mon, 3 Feb 2025 09:56:20 +0100 Subject: [PATCH 8/8] manually filter options --- .../_components/searchBox/SearchCombobox.tsx | 44 ++++++++----------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx index 602f73c43..0bcb327f0 100644 --- a/src/app/(sok)/_components/searchBox/SearchCombobox.tsx +++ b/src/app/(sok)/_components/searchBox/SearchCombobox.tsx @@ -13,6 +13,7 @@ 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; @@ -22,7 +23,8 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { const [showComboboxList, setShowComboboxList] = useState(undefined); const [windowWidth, setWindowWidth] = useState(0); const [errorMessage, setErrorMessage] = useState(null); - const [canAddNewValues, setCanAddNewValues] = useState(true); + const [optionList, setOptionList] = useState([]); + const [filteredOptions, setFilteredOptions] = useState([]); const query = useQuery(); const options = useMemo(() => getSearchBoxOptions(aggregations, locations), [aggregations, locations]); @@ -42,21 +44,17 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { return true; }; - // TODO: remove invalid options from optionList - const filteredOptions = useMemo( - () => - options - .filter((option) => isValidFreeText(option.value)) - .map((o) => { - const filterLabel = findLabelForFilter(o.value.split("-")[0]); - return filterLabel - ? { label: `${o.label} ${filterLabel}`, value: o.value } - : { label: o.label, value: o.value }; - }), - [], - ); - 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); } @@ -76,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)); } }; @@ -179,7 +172,6 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { if (isValidFreeText(option)) { handleFreeTextSearchOption(option, isSelected); } else { - setCanAddNewValues(false); setShowComboboxList(false); } } else { @@ -194,7 +186,9 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { onChange={(val) => { // Only show combobox list suggestion when user has started typing if (val.length > 0 && val.length < 100) { - setCanAddNewValues(true); + setFilteredOptions( + optionList.filter((option) => option.label.toLowerCase().includes(val.toLowerCase())), + ); setShowComboboxList(undefined); } else if (selectedOptions.length > 0) { setShowComboboxList(false); @@ -203,7 +197,7 @@ function SearchCombobox({ aggregations, locations }: SearchComboboxProps) { clearButton={false} enterKeyHint="done" shouldAutocomplete - allowNewValues={canAddNewValues} + allowNewValues isListOpen={showComboboxList} label="Legg til sted, yrker og andre søkeord" isMultiSelect