diff --git a/packages/frontend/src/App.tsx b/packages/frontend/src/App.tsx index 77fe6ae62..a2061d66d 100644 --- a/packages/frontend/src/App.tsx +++ b/packages/frontend/src/App.tsx @@ -1,10 +1,10 @@ import { Navigate, Route, Routes } from 'react-router-dom'; import { ProtectedPageLayout } from './components/PageLayout/PageLayout.tsx'; import { Inbox } from './pages/Inbox'; -import { Routes as AppRoutes } from './pages/Inbox/Inbox'; import { InboxItemPage } from './pages/InboxItemPage'; import { Logout } from './pages/LogoutPage'; import { SavedSearchesPage } from './pages/SavedSearches'; +import { PageRoutes } from './pages/routes.ts'; import './app.css'; @@ -13,13 +13,13 @@ function App() {
}> - } /> - } /> - } /> - } /> - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } /> } /> diff --git a/packages/frontend/src/api/useDialogByIdSubscription.ts b/packages/frontend/src/api/useDialogByIdSubscription.ts index d8f9f41d6..bd925bccf 100644 --- a/packages/frontend/src/api/useDialogByIdSubscription.ts +++ b/packages/frontend/src/api/useDialogByIdSubscription.ts @@ -4,7 +4,7 @@ import { useEffect } from 'react'; import { useNavigate } from 'react-router-dom'; import { SSE } from 'sse.js'; import { QUERY_KEYS } from '../constants/queryKeys.ts'; -import { Routes } from '../pages/Inbox/Inbox.tsx'; +import { PageRoutes } from '../pages/routes.ts'; export const useDialogByIdSubscription = (dialogId: string | undefined, dialogToken: string | undefined) => { const queryClient = useQueryClient(); @@ -28,7 +28,7 @@ export const useDialogByIdSubscription = (dialogId: string | undefined, dialogTo const updatedType: DialogEventType | undefined = jsonPayload.data?.dialogEvents?.type; if (updatedType && updatedType === DialogEventType.DialogDeleted) { // Redirect to inbox if the dialog was deleted - navigate(Routes.inbox); + navigate(PageRoutes.inbox); } else if (updatedType && updatedType === DialogEventType.DialogUpdated) { void queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.DIALOG_BY_ID] }); } diff --git a/packages/frontend/src/components/FilterBar/FilterBar.tsx b/packages/frontend/src/components/FilterBar/FilterBar.tsx index 25b591d43..9caa9ea47 100644 --- a/packages/frontend/src/components/FilterBar/FilterBar.tsx +++ b/packages/frontend/src/components/FilterBar/FilterBar.tsx @@ -52,7 +52,6 @@ type ListOpenTarget = 'none' | 'add_filter' | string; export type FilterBarRef = { openFilter: () => void; - resetFilters: () => void; }; /** @@ -116,9 +115,6 @@ export const FilterBar = forwardRef( openFilter() { setListOpenForTarget('add_filter'); }, - resetFilters() { - setSelectedFilters([]); - }, })); // biome-ignore lint: lint/correctness/useExhaustiveDependencies @@ -137,7 +133,7 @@ export const FilterBar = forwardRef( [selectedFilters, onFilterChange], ); - const getFilterSetting = (id: string) => settings.find((setting) => setting.id === id); + const getFilterConfig = (id: string) => settings.find((setting) => setting.id === id); /** * Toggles the filter value for a given filter ID. If `overrideValue` is `true`, @@ -151,7 +147,7 @@ export const FilterBar = forwardRef( (id: string, value: FilterValueType, overrideValue?: boolean) => { const existingFilters = selectedFilters.filter((filter) => filter.id === id); const filterExists = existingFilters.some((filter) => filter.value === value); - const setting = getFilterSetting(id); + const setting = getFilterConfig(id); const allowMultiselect = setting?.operation === 'includes'; let updatedFilters: Filter[]; @@ -200,15 +196,15 @@ export const FilterBar = forwardRef(
{Object.keys(filtersById) - .filter((id) => getFilterSetting(id) !== undefined) + .filter((id) => getFilterConfig(id) !== undefined) .map((id) => { - const setting = getFilterSetting(id)!; + const filterConfig = getFilterConfig(id)!; const isFilterMenuOpen = listOpenForTarget === id; return ( setListOpenForTarget(isFilterMenuOpen ? 'none' : id)} onBackBtnClick={() => setListOpenForTarget('add_filter')} onRemove={() => handleOnRemove(id)} diff --git a/packages/frontend/src/components/FosToolbar/FosToolbar.tsx b/packages/frontend/src/components/FosToolbar/FosToolbar.tsx index d987701a2..df4ec92c8 100644 --- a/packages/frontend/src/components/FosToolbar/FosToolbar.tsx +++ b/packages/frontend/src/components/FosToolbar/FosToolbar.tsx @@ -8,6 +8,7 @@ interface FosToolbarProps { onFilterBtnClick?: () => void; onSaveBtnClick: () => void; hideSaveButton?: boolean; + hideFilterButton?: boolean; } /* * FosToolbar is a floating toolbar that is only visible on mobile and contains action buttons for filtering, sorting and saving search. @@ -16,12 +17,17 @@ interface FosToolbarProps { * @param hideSaveButton - Optional boolean that determines if the save button should be hidden. Default is false * @returns A floating toolbar with action buttons for filtering, sorting and saving search. */ -export const FosToolbar = ({ onFilterBtnClick, onSaveBtnClick, hideSaveButton = false }: FosToolbarProps) => { +export const FosToolbar = ({ + onFilterBtnClick, + onSaveBtnClick, + hideSaveButton = false, + hideFilterButton = false, +}: FosToolbarProps) => { const { t } = useTranslation(); return (
- {onFilterBtnClick && ( + {hideFilterButton ? null : ( {t('fos.buttons.filter')} diff --git a/packages/frontend/src/components/PageLayout/GlobalMenu/useGlobalMenu.tsx b/packages/frontend/src/components/PageLayout/GlobalMenu/useGlobalMenu.tsx index 00cbf35bf..a5d828da5 100644 --- a/packages/frontend/src/components/PageLayout/GlobalMenu/useGlobalMenu.tsx +++ b/packages/frontend/src/components/PageLayout/GlobalMenu/useGlobalMenu.tsx @@ -3,7 +3,8 @@ import { useTranslation } from 'react-i18next'; import { Link, useLocation } from 'react-router-dom'; import { useWindowSize } from '../../../../utils/useWindowSize.tsx'; import type { InboxViewType } from '../../../api/useDialogs.tsx'; -import { Routes } from '../../../pages/Inbox/Inbox.tsx'; +import { getGlobalSearchQueryParams } from '../../../pages/Inbox/queryParams.ts'; +import { PageRoutes } from '../../../pages/routes.ts'; export type SideBarView = InboxViewType | 'saved-searches' | 'archive' | 'bin'; export type ItemPerViewCount = { @@ -36,6 +37,7 @@ const createMenuItemComponent = export const useGlobalMenu = ({ itemsPerViewCount }: UseSidebarProps): UseGlobalMenuProps => { const { t } = useTranslation(); const { pathname, search } = useLocation(); + const globalSearchQueryParams = getGlobalSearchQueryParams(search); const { isMobile } = useWindowSize(); const linksMenuItems: MenuItemProps[] = [ { @@ -70,10 +72,10 @@ export const useGlobalMenu = ({ itemsPerViewCount }: UseSidebarProps): UseGlobal title: t('sidebar.inbox'), color: 'strong', badge: getBadgeProps(itemsPerViewCount.inbox, 'alert'), - selected: pathname === Routes.inbox, + selected: pathname === PageRoutes.inbox, expanded: true, as: createMenuItemComponent({ - to: Routes.inbox + search, + to: PageRoutes.inbox + globalSearchQueryParams, }), items: [ { @@ -82,9 +84,9 @@ export const useGlobalMenu = ({ itemsPerViewCount }: UseSidebarProps): UseGlobal icon: 'doc-pencil', title: t('sidebar.drafts'), badge: getBadgeProps(itemsPerViewCount.drafts), - selected: pathname === Routes.drafts, + selected: pathname === PageRoutes.drafts, as: createMenuItemComponent({ - to: Routes.drafts + search, + to: PageRoutes.drafts + globalSearchQueryParams, }), }, { @@ -93,9 +95,9 @@ export const useGlobalMenu = ({ itemsPerViewCount }: UseSidebarProps): UseGlobal icon: 'file-checkmark', title: t('sidebar.sent'), badge: getBadgeProps(itemsPerViewCount.sent), - selected: pathname === Routes.sent, + selected: pathname === PageRoutes.sent, as: createMenuItemComponent({ - to: Routes.sent + search, + to: PageRoutes.sent + globalSearchQueryParams, }), }, { @@ -104,9 +106,9 @@ export const useGlobalMenu = ({ itemsPerViewCount }: UseSidebarProps): UseGlobal icon: 'bookmark', title: t('sidebar.saved_searches'), badge: getBadgeProps(itemsPerViewCount['saved-searches']), - selected: pathname === Routes.savedSearches, + selected: pathname === PageRoutes.savedSearches, as: createMenuItemComponent({ - to: Routes.savedSearches + search, + to: PageRoutes.savedSearches + globalSearchQueryParams, }), }, { @@ -115,9 +117,9 @@ export const useGlobalMenu = ({ itemsPerViewCount }: UseSidebarProps): UseGlobal icon: 'archive', title: t('sidebar.archived'), badge: getBadgeProps(itemsPerViewCount.archive), - selected: pathname === Routes.archive, + selected: pathname === PageRoutes.archive, as: createMenuItemComponent({ - to: Routes.archive + search, + to: PageRoutes.archive + globalSearchQueryParams, }), }, { @@ -126,9 +128,9 @@ export const useGlobalMenu = ({ itemsPerViewCount }: UseSidebarProps): UseGlobal icon: 'trash', title: t('sidebar.deleted'), badge: getBadgeProps(itemsPerViewCount.bin), - selected: pathname === Routes.bin, + selected: pathname === PageRoutes.bin, as: createMenuItemComponent({ - to: Routes.bin + search, + to: PageRoutes.bin + globalSearchQueryParams, }), }, ], diff --git a/packages/frontend/src/components/PageLayout/Search/useSearchDialogs.tsx b/packages/frontend/src/components/PageLayout/Search/useSearchDialogs.tsx index 5507f1211..470ce489b 100644 --- a/packages/frontend/src/components/PageLayout/Search/useSearchDialogs.tsx +++ b/packages/frontend/src/components/PageLayout/Search/useSearchDialogs.tsx @@ -32,10 +32,9 @@ export const useSearchDialogs = ({ parties, searchValue }: searchDialogsProps): }); const [searchResults, setSearchResults] = useState([] as InboxItemInput[]); - // biome-ignore lint/correctness/useExhaustiveDependencies: Full control of what triggers this code is needed useEffect(() => { setSearchResults(enabled ? mapDialogDtoToInboxItem(data?.searchDialogs?.items ?? [], parties, organizations) : []); - }, [setSearchResults, data?.searchDialogs?.items, enabled, parties, organizations]); + }, [data?.searchDialogs?.items, enabled, parties, organizations]); return { isLoading, diff --git a/packages/frontend/src/components/PageLayout/Search/useSearchString.tsx b/packages/frontend/src/components/PageLayout/Search/useSearchString.tsx index 6b829db1f..f3c1a2ae5 100644 --- a/packages/frontend/src/components/PageLayout/Search/useSearchString.tsx +++ b/packages/frontend/src/components/PageLayout/Search/useSearchString.tsx @@ -2,8 +2,8 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useEffect } from 'react'; import { useLocation, useNavigate, useSearchParams } from 'react-router-dom'; import { QUERY_KEYS } from '../../../constants/queryKeys.ts'; -import { Routes } from '../../../pages/Inbox/Inbox.tsx'; import { getSearchStringFromQueryParams } from '../../../pages/Inbox/queryParams.ts'; +import { PageRoutes } from '../../../pages/routes.ts'; export const useSearchString = () => { const [searchParams, setSearchParams] = useSearchParams(); @@ -48,10 +48,10 @@ export const useSearchString = () => { } else { const newSearchParams = new URLSearchParams(searchParams); newSearchParams.set('search', value); - if (location.pathname !== Routes.inbox) { - navigate(Routes.inbox + `?${newSearchParams.toString()}`); + if (location.pathname !== PageRoutes.inbox) { + navigate(PageRoutes.inbox + `?${newSearchParams.toString()}`); } else { - setSearchParams(newSearchParams, { replace: true }); + setSearchParams(newSearchParams); } setEnteredSearchValue(value); } @@ -61,7 +61,7 @@ export const useSearchString = () => { const newSearchParams = new URLSearchParams(searchParams); if (newSearchParams.has('search')) { newSearchParams.delete('search'); - setSearchParams(newSearchParams, { replace: true }); + setSearchParams(newSearchParams); } setSearchValue(''); setEnteredSearchValue(''); diff --git a/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx b/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx index 74cde67e2..5525422b8 100644 --- a/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx +++ b/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx @@ -1,5 +1,5 @@ import { BookmarkFillIcon, BookmarkIcon } from '@navikt/aksel-icons'; -import type { SavedSearchData, SavedSearchesFieldsFragment, SearchDataValueFilter } from 'bff-types-generated'; +import type { SavedSearchData, SearchDataValueFilter } from 'bff-types-generated'; import type { ButtonHTMLAttributes, RefAttributes } from 'react'; import { useTranslation } from 'react-i18next'; import type { Filter } from '..'; @@ -8,19 +8,7 @@ import { useParties } from '../../api/useParties'; import { useSavedSearches } from '../../pages/SavedSearches/useSavedSearches'; import { useSearchString } from '../PageLayout/Search'; import { ProfileButton } from '../ProfileButton'; -import { deepEqual } from './deepEqual'; - -const isSearchSavedAlready = ( - savedSearches: SavedSearchesFieldsFragment[], - searchDataToCheck: SavedSearchData, -): SavedSearchesFieldsFragment | undefined => { - if (!searchDataToCheck) return undefined; - return savedSearches.find((s) => - Object.keys(searchDataToCheck).every((key) => - deepEqual(s.data[key as keyof SavedSearchData], searchDataToCheck[key as keyof SavedSearchData]), - ), - ); -}; +import { getAlreadySavedSearch } from './alreadySaved.ts'; type SaveSearchButtonProps = { disabled?: boolean; @@ -46,21 +34,18 @@ export const SaveSearchButton = ({ disabled, className, activeFilters, viewType searchString: enteredSearchValue, }; - const alreadyExistingSavedSearch = isSearchSavedAlready( - savedSearches ?? ([] as SavedSearchesFieldsFragment[]), - searchToCheckIfExistsAlready, - ); + const alreadySavedSearch = getAlreadySavedSearch(searchToCheckIfExistsAlready, savedSearches); if (disabled) { return null; } - if (alreadyExistingSavedSearch) { + if (alreadySavedSearch) { return ( deleteSearch(alreadyExistingSavedSearch.id)} + onClick={() => deleteSearch(alreadySavedSearch.id)} variant="tertiary" isLoading={isCTALoading} > diff --git a/packages/frontend/src/components/SavedSearchButton/alreadySaved.spec.ts b/packages/frontend/src/components/SavedSearchButton/alreadySaved.spec.ts new file mode 100644 index 000000000..fc73fad46 --- /dev/null +++ b/packages/frontend/src/components/SavedSearchButton/alreadySaved.spec.ts @@ -0,0 +1,82 @@ +import type { SavedSearchData, SavedSearchesFieldsFragment } from 'bff-types-generated'; +import { describe, expect, it } from 'vitest'; +import { getAlreadySavedSearch } from './alreadySaved'; + +const mockedSavedSearches: SavedSearchesFieldsFragment[] = [ + { + id: 1, + name: '', + createdAt: '1734948711786', + updatedAt: '1734948711786', + data: { + urn: ['urn:altinn:organization:identifier-no:1', 'urn:altinn:organization:identifier-no:2'], + searchString: '', + fromView: '/', + filters: [ + { + id: 'sender', + value: 'Skatteetaten', + }, + { + id: 'sender', + value: 'Digitaliseringsdirektoratet', + }, + ], + }, + }, + { + id: 2, + name: '', + createdAt: '1734949347292', + updatedAt: '1734949347292', + data: { + urn: ['urn:altinn:person:identifier-no:2'], + searchString: '', + fromView: '/', + filters: [ + { + id: 'sender', + value: 'digitaliseringsdirektoratet', + }, + { + id: 'sender', + value: 'Skatteetaten', + }, + ], + }, + }, +]; + +describe('getAlreadySavedSearch', () => { + it('should return undefined when savedSearches is undefined', () => { + const searchDataToCheck: SavedSearchData = {}; + const result = getAlreadySavedSearch(searchDataToCheck, undefined); + expect(result).toBeUndefined(); + }); + + it('should return the matching saved search', () => { + const searchDataToCheck: SavedSearchData = mockedSavedSearches[1].data; + const result = getAlreadySavedSearch(searchDataToCheck, mockedSavedSearches); + expect(result?.id).toEqual(2); + }); + + it('should return the matching saved search disregarding order of keys and values', () => { + const mockedSearchDataToCheck: SavedSearchData = { + urn: ['urn:altinn:organization:identifier-no:2', 'urn:altinn:organization:identifier-no:1'], + searchString: '', + fromView: '/', + filters: [ + { + id: 'sender', + value: 'Digitaliseringsdirektoratet', + }, + { + id: 'sender', + value: 'Skatteetaten', + }, + ], + }; + const result = getAlreadySavedSearch(mockedSearchDataToCheck, mockedSavedSearches); + expect(result?.id).toEqual(1); + }); +}); diff --git a/packages/frontend/src/components/SavedSearchButton/alreadySaved.ts b/packages/frontend/src/components/SavedSearchButton/alreadySaved.ts new file mode 100644 index 000000000..8222c6560 --- /dev/null +++ b/packages/frontend/src/components/SavedSearchButton/alreadySaved.ts @@ -0,0 +1,71 @@ +import type { SavedSearchData, SavedSearchesFieldsFragment } from 'bff-types-generated'; + +type JsonValue = string | number | boolean | JsonObject | JsonArray | null; +type JsonObject = { [key: string]: JsonValue }; +type JsonArray = JsonValue[]; + +const deepEqual = (value1: unknown, value2: unknown): boolean => { + if (value1 === null && value2 === null) return true; + + if (typeof value1 !== typeof value2) return false; + + if (typeof value1 === 'object' && typeof value2 === 'object') { + if (Array.isArray(value1) && Array.isArray(value2)) { + return arraysAreEqual(value1, value2); + } + + if (!Array.isArray(value1) && !Array.isArray(value2)) { + return objectsAreEqual(value1 as JsonObject, value2 as JsonObject); + } + } + + return value1 === value2; +}; + +const objectsAreEqual = (obj1: JsonObject, obj2: JsonObject): boolean => { + if (obj1 === null || obj2 === null) return obj1 === obj2; + + const keys1 = Object.keys(obj1).sort(); + const keys2 = Object.keys(obj2).sort(); + + if (keys1.length !== keys2.length) return false; + + return keys1.every((key) => deepEqual(obj1[key], obj2[key])); +}; + +const arraysAreEqual = (arr1: JsonArray, arr2: JsonArray): boolean => { + if (arr1.length !== arr2.length) return false; + + const normalize = (item: JsonValue): string => { + if (typeof item === 'object' && item !== null && !Array.isArray(item)) { + return JSON.stringify( + Object.keys(item) + .sort() + .reduce((acc, key) => { + acc[key] = item[key]; + return acc; + }, {} as JsonObject), + ); + } + return JSON.stringify(item); + }; + + const sortedArr1 = arr1.map(normalize).sort(); + const sortedArr2 = arr2.map(normalize).sort(); + + return sortedArr1.every((item, index) => item === sortedArr2[index]); +}; + +export const getAlreadySavedSearch = ( + searchDataToCheck: SavedSearchData, + savedSearches: SavedSearchesFieldsFragment[] | undefined, +): SavedSearchesFieldsFragment | undefined => { + return (savedSearches ?? []).find((prevSaved) => { + const prevSavedData = prevSaved.data as SavedSearchData; + return Object.keys(searchDataToCheck).every((key) => { + const prevSavedDataKey = prevSavedData[key as keyof SavedSearchData]; + const searchDataToCheckKey = searchDataToCheck[key as keyof SavedSearchData]; + return deepEqual(prevSavedDataKey, searchDataToCheckKey); + }); + }); +}; diff --git a/packages/frontend/src/components/SavedSearchButton/deepEqual.ts b/packages/frontend/src/components/SavedSearchButton/deepEqual.ts deleted file mode 100644 index 086b9b962..000000000 --- a/packages/frontend/src/components/SavedSearchButton/deepEqual.ts +++ /dev/null @@ -1,37 +0,0 @@ -type IndexedObject = { [key: string]: T }; - -export const deepEqual = (obj1: unknown, obj2: unknown): boolean => { - if (obj1 === obj2) return true; - - if (!obj1 || !obj2 || typeof obj1 !== 'object' || typeof obj2 !== 'object') { - return false; - } - - const keys1 = Object.keys(obj1); - const keys2 = Object.keys(obj2); - - if (keys1.length !== keys2.length) return false; - - const obj1Typed = obj1 as IndexedObject; - const obj2Typed = obj2 as IndexedObject; - - for (const [key, value1] of Object.entries(obj1Typed)) { - const value2 = obj2Typed[key]; - - if (value2 === undefined) return false; - - if (value1 instanceof Date && value2 instanceof Date) { - if (value1.getTime() !== value2.getTime()) return false; - continue; - } - - if (value1 instanceof RegExp && value2 instanceof RegExp) { - if (value1.toString() !== value2.toString()) return false; - continue; - } - - if (!deepEqual(value1, value2)) return false; - } - - return true; -}; diff --git a/packages/frontend/src/pages/Inbox/Inbox.tsx b/packages/frontend/src/pages/Inbox/Inbox.tsx index 0fb389a4a..275e63f64 100644 --- a/packages/frontend/src/pages/Inbox/Inbox.tsx +++ b/packages/frontend/src/pages/Inbox/Inbox.tsx @@ -1,15 +1,14 @@ import { ArrowForwardIcon, ClockDashedIcon, EnvelopeOpenIcon, TrashIcon } from '@navikt/aksel-icons'; -import { useEffect, useMemo, useRef, useState } from 'react'; +import { format } from 'date-fns'; +import { useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { type InboxViewType, getViewType, useDialogs } from '../../api/useDialogs.tsx'; +import { type InboxViewType, useDialogs } from '../../api/useDialogs.tsx'; import { useParties } from '../../api/useParties.ts'; import { ActionPanel, - type Filter, FilterBar, InboxItem, - type InboxItemInput, InboxItems, PartyDropdown, useSelectedDialogs, @@ -21,122 +20,49 @@ import { InboxItemsHeader } from '../../components/InboxItem/InboxItemsHeader.ts import { useSearchDialogs, useSearchString } from '../../components/PageLayout/Search/'; import { SaveSearchButton } from '../../components/SavedSearchButton/SaveSearchButton.tsx'; import { FeatureFlagKeys, useFeatureFlag } from '../../featureFlags'; -import { useFormat } from '../../i18n/useDateFnsLocale.tsx'; import { useSavedSearches } from '../SavedSearches/useSavedSearches.ts'; import { InboxSkeleton } from './InboxSkeleton.tsx'; -import { filterDialogs, getFilterBarSettings } from './filters.ts'; +import { filterDialogs } from './filters.ts'; import styles from './inbox.module.css'; -import { useFilterResetOnSelectedPartiesChange } from './useFilterResetOnSelectedPartiesChange.ts'; -import { useSetFiltersOnLocationChange } from './useSetFiltersOnLocationChange.ts'; +import { useFilters } from './useFilters.tsx'; +import useGroupedDialogs from './useGroupedDialogs.tsx'; interface InboxProps { viewType: InboxViewType; } -export enum Routes { - inbox = '/', - inboxItem = '/inbox/:id', - sent = '/sent', - drafts = '/drafts', - savedSearches = '/saved-searches', - archive = '/archive', - bin = '/bin', -} - -interface DialogCategory { - label: string; - id: string; - items: InboxItemInput[]; -} - export const Inbox = ({ viewType }: InboxProps) => { - const format = useFormat(); + const { openSnackbar } = useSnackbar(); const { t } = useTranslation(); const filterBarRef = useRef(null); const disableBulkActions = useFeatureFlag(FeatureFlagKeys.DisableBulkActions); const location = useLocation(); const { selectedItems, setSelectedItems, selectedItemCount, inSelectionMode } = useSelectedDialogs(); - const { openSnackbar } = useSnackbar(); const { selectedParties, selectedPartyIds } = useParties(); const { enteredSearchValue } = useSearchString(); - const [initialFilters, setInitialFilters] = useState([]); - const [activeFilters, setActiveFilters] = useState([]); const { saveSearch } = useSavedSearches(selectedPartyIds); - const { searchResults, isFetching: isFetchingSearchResults } = useSearchDialogs({ + const { + searchResults, + isFetching: isFetchingSearchResults, + isSuccess: searchSuccess, + } = useSearchDialogs({ parties: selectedParties, searchValue: enteredSearchValue, }); const { dialogsByView, isLoading: isLoadingDialogs, isSuccess: dialogsIsSuccess } = useDialogs(selectedParties); const dialogsForView = dialogsByView[viewType]; - - const showingSearchResults = enteredSearchValue.length > 0; - const dataSource = showingSearchResults ? searchResults : dialogsForView; - useFilterResetOnSelectedPartiesChange({ setActiveFilters, selectedParties }); - useSetFiltersOnLocationChange({ setInitialFilters }); - - const shouldShowSearchResults = !isFetchingSearchResults && showingSearchResults; - - // biome-ignore lint/correctness/useExhaustiveDependencies: Full control of what triggers this code is needed - useEffect(() => { - if (showingSearchResults && activeFilters.length) { - filterBarRef.current?.resetFilters(); - } - }, [showingSearchResults]); - - // biome-ignore lint/correctness/useExhaustiveDependencies: Full control of what triggers this code is needed - const itemsToDisplay = useMemo(() => { - return filterDialogs(dataSource, activeFilters, format); - }, [dataSource, activeFilters]); - - const filterBarSettings = getFilterBarSettings(dataSource, activeFilters, format).filter( - (setting) => - setting.options.length > 1 || - typeof activeFilters.find((filter) => filter.id === setting.id) !== 'undefined' || - setting.id === 'updated', - ); - - // biome-ignore lint/correctness/useExhaustiveDependencies: Full control of what triggers this code is needed - const dialogsGroupedByCategory: DialogCategory[] = useMemo(() => { - const allWithinSameYear = itemsToDisplay.every( - (d) => new Date(d.createdAt).getFullYear() === new Date().getFullYear(), - ); - - const youAreNotInInbox = itemsToDisplay.every((d) => ['drafts', 'sent', 'bin', 'archive'].includes(getViewType(d))); - if (!shouldShowSearchResults && youAreNotInInbox) { - return [ - { - label: t(`inbox.heading.title.${viewType}`, { count: itemsToDisplay.length }), - id: viewType, - items: itemsToDisplay, - }, - ]; - } - - return itemsToDisplay.reduce((acc, item, _, list) => { - const createdAt = new Date(item.createdAt); - const viewType = getViewType(item); - const key = shouldShowSearchResults - ? viewType - : allWithinSameYear - ? format(createdAt, 'LLLL') - : format(createdAt, 'yyyy'); - - const label = shouldShowSearchResults - ? t(`inbox.heading.search_results.${key}`, { count: list.filter((i) => getViewType(i) === key).length }) - : key; - - const existingCategory = acc.find((c: { id: string }) => c.id === key); - - if (existingCategory) { - existingCategory.items.push(item); - } else { - acc.push({ label, id: key, items: [item] }); - } - - return acc; - }, [] as DialogCategory[]); - }, [itemsToDisplay, shouldShowSearchResults]); + const displaySearchResults = enteredSearchValue.length > 0; + const dataSource = displaySearchResults ? searchResults : dialogsForView; + const dataSourceFetchSuccess = displaySearchResults ? searchSuccess : dialogsIsSuccess; + const { filters, onFiltersChange, filterSettings } = useFilters({ dialogs: dataSource }); + const filteredItems = useMemo(() => filterDialogs(dataSource, filters, format), [dataSource, filters]); + const dialogsGroupedByCategory = useGroupedDialogs({ + items: filteredItems, + displaySearchResults, + filters, + viewType, + }); const handleCheckedChange = (checkboxValue: string, checked: boolean) => { setSelectedItems((prev: Record) => ({ @@ -145,37 +71,33 @@ export const Inbox = ({ viewType }: InboxProps) => { })); }; - const savedSearchDisabled = !activeFilters?.length && !enteredSearchValue; - const showFilterButton = filterBarSettings.length > 0; - - if (isFetchingSearchResults) { - return ( -
- -
- ); - } + const savedSearchDisabled = !filters?.length && !enteredSearchValue; + const showFilterButton = filterSettings.length > 0; - if (isLoadingDialogs) { - return ( -
- -
- ); + if (isFetchingSearchResults || isLoadingDialogs) { + return ; } - if (itemsToDisplay.length === 0 && dialogsIsSuccess) { + if (filteredItems.length === 0 && dataSourceFetchSuccess) { return ( -
+
+
-
+
); } @@ -187,31 +109,28 @@ export const Inbox = ({ viewType }: InboxProps) => {
{ - filterBarRef?.current?.openFilter(); - } - : undefined - } + onFilterBtnClick={() => { + filterBarRef?.current?.openFilter(); + }} onSaveBtnClick={() => - saveSearch({ filters: activeFilters, selectedParties: selectedPartyIds, enteredSearchValue, viewType }) + saveSearch({ filters, selectedParties: selectedPartyIds, enteredSearchValue, viewType }) } + hideFilterButton={!showFilterButton} hideSaveButton={savedSearchDisabled} />
diff --git a/packages/frontend/src/pages/Inbox/filters.ts b/packages/frontend/src/pages/Inbox/filters.ts index ef98fd2a8..ae21158a7 100644 --- a/packages/frontend/src/pages/Inbox/filters.ts +++ b/packages/frontend/src/pages/Inbox/filters.ts @@ -74,6 +74,21 @@ export const filterDialogs = ( }); }; +export enum FilterBarIds { + SENDER = 'sender', + RECEIVER = 'receiver', + STATUS = 'status', + UPDATED = 'updated', +} + +/** + * Generates filter settings for the filter bar. + * + * @param {InboxItemInput[]} dialogs - The array of dialogs to filter. + * @param {Array} activeFilters - The array of active filter objects, where each filter has an 'id' and a 'value'. + * @param format + * @returns {Array} - The array of filter settings. + */ export const getFilterBarSettings = ( dialogs: InboxItemInput[], activeFilters: Filter[], @@ -81,7 +96,7 @@ export const getFilterBarSettings = ( ): FilterSetting[] => { return [ { - id: 'sender', + id: FilterBarIds.SENDER, label: t('filter_bar.label.sender'), unSelectedLabel: t('filter_bar.label.all_senders'), mobileNavLabel: t('filter_bar.label.choose_sender'), @@ -89,9 +104,7 @@ export const getFilterBarSettings = ( options: (() => { const otherFilters = activeFilters.filter((activeFilter) => activeFilter.id !== 'sender'); const filteredDialogs = filterDialogs(dialogs, otherFilters, format); - const senders = filteredDialogs.map((p) => p.sender.name); - const senderCounts = countOccurrences(senders); return Array.from(new Set(senders)).map((sender) => ({ @@ -102,7 +115,7 @@ export const getFilterBarSettings = ( })(), }, { - id: 'receiver', + id: FilterBarIds.RECEIVER, label: t('filter_bar.label.recipient'), unSelectedLabel: t('filter_bar.label.all_recipients'), mobileNavLabel: t('filter_bar.label.choose_recipient'), @@ -121,7 +134,7 @@ export const getFilterBarSettings = ( })(), }, { - id: 'status', + id: FilterBarIds.STATUS, label: t('filter_bar.label.status'), unSelectedLabel: t('filter_bar.label.all_statuses'), mobileNavLabel: t('filter_bar.label.choose_status'), @@ -142,7 +155,7 @@ export const getFilterBarSettings = ( })(), }, { - id: 'updated', + id: FilterBarIds.UPDATED, label: t('filter_bar.label.updated'), mobileNavLabel: t('filter_bar.label.choose_date'), unSelectedLabel: t('filter_bar.label.all_dates'), @@ -154,3 +167,28 @@ export const getFilterBarSettings = ( }, ]; }; + +export const createFiltersURLQuery = (activeFilters: Filter[], allFilterKeys: string[], baseURL: string): URL => { + const url = new URL(baseURL); + + for (const filter of allFilterKeys) { + url.searchParams.delete(filter); + } + + for (const filter of activeFilters.filter((filter) => typeof filter.value !== 'undefined')) { + url.searchParams.append(filter.id, String(filter.value)); + } + return url; +}; + +export const readFiltersFromURLQuery = (query: string): Filter[] => { + const searchParams = new URLSearchParams(query); + const allowedFilterKeys = Object.values(FilterBarIds) as string[]; + const filters: Filter[] = []; + searchParams.forEach((value, key) => { + if (allowedFilterKeys.includes(key)) { + filters.push({ id: key, value }); + } + }); + return filters; +}; diff --git a/packages/frontend/src/pages/Inbox/queryParams.ts b/packages/frontend/src/pages/Inbox/queryParams.ts index e5732d549..0a5d50ddb 100644 --- a/packages/frontend/src/pages/Inbox/queryParams.ts +++ b/packages/frontend/src/pages/Inbox/queryParams.ts @@ -1,24 +1,32 @@ -import type { Filter } from '../../components'; - -export const getFiltersFromQueryParams = (searchParams: URLSearchParams): Filter[] => { - const compressedData = searchParams.get('filters'); - return compressedData ? JSON.parse(compressedData) : ([] as Filter[]); -}; - -export const getQueryParamsWithoutFilters = (): URLSearchParams => { - const searchParams = new URLSearchParams(window.location.search); - searchParams.delete('filters'); - return searchParams; +const GlobalQueryParams = { + search: 'search', + party: 'party', + allParties: 'allParties', + mock: 'mock', }; export const getSearchStringFromQueryParams = (searchParams: URLSearchParams): string => { - return searchParams.get('search') || ''; + return searchParams.get(GlobalQueryParams.search) || ''; }; export const getSelectedPartyFromQueryParams = (searchParams: URLSearchParams): string => { - return decodeURIComponent(searchParams.get('party') || ''); + return decodeURIComponent(searchParams.get(GlobalQueryParams.party) || ''); }; export const getSelectedAllPartiesFromQueryParams = (searchParams: URLSearchParams): boolean => { - return searchParams.get('allParties') === 'true'; + return searchParams.get(GlobalQueryParams.allParties) === 'true'; +}; + +/* except current location.search and returns location.search only with GlobalQueryParams if provided in location.search */ +export const getGlobalSearchQueryParams = (search: string): string => { + const searchParams = new URLSearchParams(search); + const globalQueryParams = new URLSearchParams(); + + for (const key of Object.values(GlobalQueryParams)) { + if (searchParams.has(key)) { + globalQueryParams.set(key, searchParams.get(key)!); + } + } + + return globalQueryParams.toString() === '' ? '' : `?${globalQueryParams.toString()}`; }; diff --git a/packages/frontend/src/pages/Inbox/useFilterResetOnSelectedPartiesChange.ts b/packages/frontend/src/pages/Inbox/useFilterResetOnSelectedPartiesChange.ts deleted file mode 100644 index 621e8af7e..000000000 --- a/packages/frontend/src/pages/Inbox/useFilterResetOnSelectedPartiesChange.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { PartyFieldsFragment } from 'bff-types-generated'; -import { type Dispatch, type SetStateAction, useEffect, useRef } from 'react'; -import { useSearchParams } from 'react-router-dom'; -import type { Filter, FilterBarRef } from '../../components/FilterBar/FilterBar'; -import { getQueryParamsWithoutFilters } from './queryParams'; - -interface UseFilterResetOnSelectedPartiesChangeProps { - setActiveFilters: Dispatch>; - selectedParties: PartyFieldsFragment[]; -} - -export const useFilterResetOnSelectedPartiesChange = ({ - setActiveFilters, - selectedParties, -}: UseFilterResetOnSelectedPartiesChangeProps) => { - const filterBarRef = useRef(null); - const [, setSearchParams] = useSearchParams(); - - const resetAllFilters = () => { - setActiveFilters([]); - setSearchParams(getQueryParamsWithoutFilters()); - filterBarRef.current?.resetFilters(); - }; - - // biome-ignore lint/correctness/useExhaustiveDependencies: Full control of what triggers this code is needed - useEffect(() => { - resetAllFilters(); - }, [selectedParties]); -}; diff --git a/packages/frontend/src/pages/Inbox/useFilters.tsx b/packages/frontend/src/pages/Inbox/useFilters.tsx new file mode 100644 index 000000000..f7c2f5766 --- /dev/null +++ b/packages/frontend/src/pages/Inbox/useFilters.tsx @@ -0,0 +1,50 @@ +import { useMemo, useState } from 'react'; +import { useSearchParams } from 'react-router-dom'; +import type { Filter, InboxItemInput } from '../../components'; +import type { FilterSetting } from '../../components/FilterBar/FilterBar.tsx'; +import { useFormat } from '../../i18n/useDateFnsLocale.tsx'; +import { createFiltersURLQuery, getFilterBarSettings, readFiltersFromURLQuery } from './filters.ts'; + +interface UseFiltersOutput { + filters: Filter[]; + filterSettings: FilterSetting[]; + onFiltersChange: (filters: Filter[]) => void; +} + +interface UseFiltersProps { + dialogs: InboxItemInput[]; +} + +export const useFilters = ({ dialogs }: UseFiltersProps): UseFiltersOutput => { + const [_, setSearchParams] = useSearchParams(); + const [filters, setFilters] = useState(readFiltersFromURLQuery(location.search)); + const format = useFormat(); + + // biome-ignore lint/correctness/useExhaustiveDependencies: + const filterSettings = useMemo(() => { + const settings = getFilterBarSettings(dialogs, filters, format).filter( + (setting) => + setting.options.length > 1 || + typeof filters.find((filter) => filter.id === setting.id) !== 'undefined' || + setting.id === 'updated', + ); + const legalFilterKeys = settings.map((setting) => setting.id); + const containsIllegalFilter = filters.some((filter) => !legalFilterKeys.includes(filter.id)); + + if (containsIllegalFilter) { + setFilters(filters.filter((filter) => legalFilterKeys.includes(filter.id))); + } + + return settings; + }, [dialogs, filters]); + + const onFiltersChange = (filters: Filter[]) => { + const currentURL = new URL(window.location.href); + const filterKeys = filterSettings.map((setting) => setting.id); + const updatedURL = createFiltersURLQuery(filters, filterKeys, currentURL.toString()); + setSearchParams(updatedURL.searchParams, { replace: true }); + setFilters(filters); + }; + + return { filters, filterSettings, onFiltersChange }; +}; diff --git a/packages/frontend/src/pages/Inbox/useGroupedDialogs.tsx b/packages/frontend/src/pages/Inbox/useGroupedDialogs.tsx new file mode 100644 index 000000000..a726e2fca --- /dev/null +++ b/packages/frontend/src/pages/Inbox/useGroupedDialogs.tsx @@ -0,0 +1,64 @@ +import { format } from 'date-fns'; +import { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { type InboxViewType, getViewType } from '../../api/useDialogs.tsx'; +import type { Filter, InboxItemInput } from '../../components'; + +interface DialogCategory { + label: string; + id: string; + items: InboxItemInput[]; +} + +interface UseGroupedDialogsProps { + items: InboxItemInput[]; + filters: Filter[]; + displaySearchResults: boolean; + viewType: InboxViewType; +} + +const useGroupedDialogs = ({ items, displaySearchResults, viewType }: UseGroupedDialogsProps): DialogCategory[] => { + const { t } = useTranslation(); + + return useMemo(() => { + const allWithinSameYear = items.every((d) => new Date(d.createdAt).getFullYear() === new Date().getFullYear()); + + const youAreNotInInbox = items.every((d) => ['drafts', 'sent', 'bin', 'archive'].includes(getViewType(d))); + + if (!displaySearchResults && youAreNotInInbox) { + return [ + { + label: t(`inbox.heading.title.${viewType}`, { count: items.length }), + id: viewType, + items, + }, + ]; + } + + return items.reduce((acc: DialogCategory[], item, _, list) => { + const createdAt = new Date(item.createdAt); + const viewType = getViewType(item); + const key = displaySearchResults + ? viewType + : allWithinSameYear + ? format(createdAt, 'LLLL') + : format(createdAt, 'yyyy'); + + const label = displaySearchResults + ? t(`inbox.heading.search_results.${key}`, { count: list.filter((i) => getViewType(i) === key).length }) + : key; + + const existingCategory = acc.find((c) => c.id === key); + + if (existingCategory) { + existingCategory.items.push(item); + } else { + acc.push({ label, id: key, items: [item] }); + } + + return acc; + }, []); + }, [items, displaySearchResults]); +}; + +export default useGroupedDialogs; diff --git a/packages/frontend/src/pages/Inbox/useSetFiltersOnLocationChange.ts b/packages/frontend/src/pages/Inbox/useSetFiltersOnLocationChange.ts deleted file mode 100644 index 36058ddae..000000000 --- a/packages/frontend/src/pages/Inbox/useSetFiltersOnLocationChange.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { type Dispatch, type SetStateAction, useEffect } from 'react'; -import { useLocation, useSearchParams } from 'react-router-dom'; -import type { Filter } from '../../components/FilterBar/FilterBar'; -import { getFiltersFromQueryParams } from './queryParams'; - -interface UseSetFiltersOnLocationChangeProps { - setInitialFilters: Dispatch>; -} - -export const useSetFiltersOnLocationChange = ({ setInitialFilters }: UseSetFiltersOnLocationChangeProps) => { - const location = useLocation(); - const [searchParams] = useSearchParams(); - - // biome-ignore lint/correctness/useExhaustiveDependencies: Full control of what triggers this code is needed - useEffect(() => { - setInitialFilters(getFiltersFromQueryParams(searchParams)); - }, [location.pathname]); -}; diff --git a/packages/frontend/src/pages/SavedSearches/SavedSearchesItem/SavedSearchesItem.tsx b/packages/frontend/src/pages/SavedSearches/SavedSearchesItem/SavedSearchesItem.tsx index 3f2b6f53d..615677b5c 100644 --- a/packages/frontend/src/pages/SavedSearches/SavedSearchesItem/SavedSearchesItem.tsx +++ b/packages/frontend/src/pages/SavedSearches/SavedSearchesItem/SavedSearchesItem.tsx @@ -1,8 +1,8 @@ import type { SavedSearchesFieldsFragment } from 'bff-types-generated'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; -import { HorizontalLine } from '../../../components'; -import { PlusIcon } from '../../../components/Icons/PlusIcon/PlusIcon.tsx'; +import { type Filter, HorizontalLine } from '../../../components'; +import { PlusIcon } from '../../../components/Icons'; import SearchFilterTag from '../SearchFilterTag/SearchFilterTag.tsx'; import styles from './savedSearchesItem.module.css'; @@ -19,10 +19,13 @@ export const SavedSearchesItem = ({ savedSearch, actionPanel, isLast }: SavedSea const urlParams = new URLSearchParams(window.location.search); const queryParams = new URLSearchParams({ ...(searchString && { search: searchString }), - ...(filters?.length && { filters: JSON.stringify(filters) }), ...Object.fromEntries(urlParams.entries()), }); + for (const filter of filters as Filter[]) { + queryParams.append(filter.id, String(filter.value)); + } + const searchData = savedSearch.data; const { t } = useTranslation(); diff --git a/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts b/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts index 793d7ddfd..ed1738a2a 100644 --- a/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts +++ b/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts @@ -11,7 +11,7 @@ import { createSavedSearch, deleteSavedSearch, fetchSavedSearches } from '../../ import type { InboxViewType } from '../../api/useDialogs.tsx'; import { type Filter, useSnackbar } from '../../components'; import { QUERY_KEYS } from '../../constants/queryKeys.ts'; -import { Routes } from '../Inbox/Inbox.tsx'; +import { PageRoutes } from '../routes.ts'; interface UseSavedSearchesOutput { savedSearches: SavedSearchesFieldsFragment[]; @@ -64,7 +64,7 @@ export const useSavedSearches = (selectedPartyIds?: string[]): UseSavedSearchesO staleTime: 1000 * 60 * 20, }); - const savedSearchesUnfiltered = data?.savedSearches as SavedSearchesFieldsFragment[]; + const savedSearchesUnfiltered = (data?.savedSearches ?? []) as SavedSearchesFieldsFragment[]; const currentPartySavedSearches = filterSavedSearches(savedSearchesUnfiltered, selectedPartyIds || []); const saveSearch = async ({ @@ -79,7 +79,7 @@ export const useSavedSearches = (selectedPartyIds?: string[]): UseSavedSearchesO filters: filters as SearchDataValueFilter[], urn: selectedParties, searchString: enteredSearchValue, - fromView: Routes[viewType], + fromView: PageRoutes[viewType], }; await createSavedSearch('', data); openSnackbar({ diff --git a/packages/frontend/src/pages/routes.ts b/packages/frontend/src/pages/routes.ts new file mode 100644 index 000000000..df3910d39 --- /dev/null +++ b/packages/frontend/src/pages/routes.ts @@ -0,0 +1,9 @@ +export enum PageRoutes { + inbox = '/', + inboxItem = '/inbox/:id', + sent = '/sent', + drafts = '/drafts', + savedSearches = '/saved-searches', + archive = '/archive', + bin = '/bin', +} diff --git a/packages/frontend/tests/stories/filter.spec.ts b/packages/frontend/tests/stories/filter.spec.ts index 0026d173c..b1750a43d 100644 --- a/packages/frontend/tests/stories/filter.spec.ts +++ b/packages/frontend/tests/stories/filter.spec.ts @@ -2,19 +2,29 @@ import { expect, test } from '@playwright/test'; import { appURL } from '../'; test.describe('Testing filter bar', () => { - test('Selecting Avsender filter and status filter', async ({ page }) => { + test('should filter when selecting sender filter and status filter', async ({ page }) => { await page.goto(appURL); + + /* Choose Skatteetaten as sender */ await page.getByRole('button', { name: 'Legg til filter' }).click(); await page.getByText('Avsender').click(); await page.getByLabel('Fra Skatteetaten').check(); await page.mouse.click(200, 0, { button: 'left' }); + + expect(new URL(page.url()).searchParams.get('sender')).toEqual('Skatteetaten'); await expect(page.getByRole('link', { name: 'Skatten din for 2022' })).toBeVisible(); + + /* Remove filter */ await page .locator('div') .filter({ hasText: /^Fra Skatteetaten Legg til filter$/ }) .getByRole('button') .nth(1) .click(); + + expect(new URL(page.url()).searchParams.has('sender')).toEqual(false); + + /* Choose COMPLETED as status */ await page.getByRole('button', { name: 'Legg til filter' }).click(); await page .locator('div') @@ -22,8 +32,47 @@ test.describe('Testing filter bar', () => { .nth(1) .click(); await page.getByText('Avsluttet').first().click(); + + expect(new URL(page.url()).searchParams.get('status')).toEqual('COMPLETED'); + await page.mouse.click(200, 0, { button: 'left' }); await expect(page.getByRole('link', { name: 'Skatten din for 2022' })).toBeVisible(); await expect(page.getByRole('link', { name: 'Søknad om personlig bilskilt' })).toBeVisible(); }); + + test('should remove filters when changing view types', async ({ page }) => { + await page.goto(appURL); + + /* Choose Skatteetaten as sender */ + await page.getByRole('button', { name: 'Legg til filter' }).click(); + await page.getByText('Avsender').click(); + await page.getByLabel('Fra Skatteetaten').check(); + await page.mouse.click(200, 0, { button: 'left' }); + + expect(new URL(page.url()).searchParams.get('sender')).toEqual('Skatteetaten'); + await expect(page.getByRole('link', { name: 'Skatten din for 2022' })).toBeVisible(); + + /* Change view type */ + await page.getByRole('menuitem', { name: 'Utkast' }).click(); + + expect(new URL(page.url()).searchParams.has('sender')).toEqual(false); + }); + + test('should keep filters when returning to a filtered inbox from ', async ({ page }) => { + await page.goto(appURL); + + /* Choose Skatteetaten as sender */ + await page.getByRole('button', { name: 'Legg til filter' }).click(); + await page.getByText('Avsender').click(); + await page.getByLabel('Fra Skatteetaten').check(); + await page.mouse.click(200, 0, { button: 'left' }); + + expect(new URL(page.url()).searchParams.get('sender')).toEqual('Skatteetaten'); + + await page.getByRole('link', { name: 'Skatten din for 2022' }).click(); + + await page.getByRole('button', { name: 'Tilbake' }).click(); + + expect(new URL(page.url()).searchParams.get('sender')).toEqual('Skatteetaten'); + }); });