From ec9e2325e15088c5c4fc9d1e02c5d99deaa47bbb Mon Sep 17 00:00:00 2001 From: Sean Erik Scully Date: Mon, 16 Dec 2024 13:04:12 +0100 Subject: [PATCH] fix: unable to save search (#1581) --- .../SavedSearchButton/SaveSearchButton.tsx | 59 ++++-------- packages/frontend/src/pages/Inbox/Inbox.tsx | 26 ++--- .../pages/SavedSearches/SavedSearchesPage.tsx | 59 +----------- .../pages/SavedSearches/useSavedSearches.ts | 94 ++++++++++++++++++- 4 files changed, 118 insertions(+), 120 deletions(-) diff --git a/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx b/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx index f5c2c70b..74cde67e 100644 --- a/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx +++ b/packages/frontend/src/components/SavedSearchButton/SaveSearchButton.tsx @@ -1,16 +1,12 @@ import { BookmarkFillIcon, BookmarkIcon } from '@navikt/aksel-icons'; -import { useQueryClient } from '@tanstack/react-query'; import type { SavedSearchData, SavedSearchesFieldsFragment, SearchDataValueFilter } from 'bff-types-generated'; import type { ButtonHTMLAttributes, RefAttributes } from 'react'; -import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import type { Filter } from '..'; -import { useSnackbar } from '..'; -import { deleteSavedSearch } from '../../api/queries'; +import type { InboxViewType } from '../../api/useDialogs.tsx'; import { useParties } from '../../api/useParties'; -import { QUERY_KEYS } from '../../constants/queryKeys'; import { useSavedSearches } from '../../pages/SavedSearches/useSavedSearches'; -import { useSearchString } from '../PageLayout/Search/useSearchString.tsx'; +import { useSearchString } from '../PageLayout/Search'; import { ProfileButton } from '../ProfileButton'; import { deepEqual } from './deepEqual'; @@ -27,27 +23,22 @@ const isSearchSavedAlready = ( }; type SaveSearchButtonProps = { - onBtnClick: () => Promise; - isLoading?: boolean; disabled?: boolean; + viewType: InboxViewType; activeFilters: Filter[]; } & ButtonHTMLAttributes & RefAttributes; -export const SaveSearchButton = ({ - disabled, - onBtnClick, - className, - isLoading, - activeFilters, -}: SaveSearchButtonProps) => { +export const SaveSearchButton = ({ disabled, className, activeFilters, viewType }: SaveSearchButtonProps) => { const { t } = useTranslation(); const { selectedPartyIds } = useParties(); const { enteredSearchValue } = useSearchString(); - const [isDeleting, setIsDeleting] = useState(false); - const { currentPartySavedSearches: savedSearches } = useSavedSearches(selectedPartyIds); - const { openSnackbar } = useSnackbar(); - const queryClient = useQueryClient(); + const { + currentPartySavedSearches: savedSearches, + isCTALoading, + saveSearch, + deleteSearch, + } = useSavedSearches(selectedPartyIds); const searchToCheckIfExistsAlready: SavedSearchData = { filters: activeFilters as SearchDataValueFilter[], @@ -60,26 +51,6 @@ export const SaveSearchButton = ({ searchToCheckIfExistsAlready, ); - const handleDeleteSearch = async (savedSearchId: number) => { - setIsDeleting(true); - try { - await deleteSavedSearch(savedSearchId); - openSnackbar({ - message: t('savedSearches.deleted_success'), - variant: 'success', - }); - await queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.SAVED_SEARCHES] }); - } catch (error) { - console.error('Failed to delete saved search:', error); - openSnackbar({ - message: t('savedSearches.delete_failed'), - variant: 'error', - }); - } finally { - setIsDeleting(false); - } - }; - if (disabled) { return null; } @@ -89,9 +60,9 @@ export const SaveSearchButton = ({ handleDeleteSearch(alreadyExistingSavedSearch.id)} + onClick={() => deleteSearch(alreadyExistingSavedSearch.id)} variant="tertiary" - isLoading={isLoading || isDeleting} + isLoading={isCTALoading} > {t('filter_bar.saved_search')} @@ -103,9 +74,11 @@ export const SaveSearchButton = ({ + saveSearch({ filters: activeFilters, selectedParties: selectedPartyIds, enteredSearchValue, viewType }) + } variant="tertiary" - isLoading={isLoading || isDeleting} + isLoading={isCTALoading} > {t('filter_bar.save_search')} diff --git a/packages/frontend/src/pages/Inbox/Inbox.tsx b/packages/frontend/src/pages/Inbox/Inbox.tsx index 560d47bb..4a9a4f31 100644 --- a/packages/frontend/src/pages/Inbox/Inbox.tsx +++ b/packages/frontend/src/pages/Inbox/Inbox.tsx @@ -22,7 +22,7 @@ import { useSearchDialogs, useSearchString } from '../../components/PageLayout/S import { SaveSearchButton } from '../../components/SavedSearchButton/SaveSearchButton.tsx'; import { FeatureFlagKeys, useFeatureFlag } from '../../featureFlags'; import { useFormat } from '../../i18n/useDateFnsLocale.tsx'; -import { handleSaveSearch } from '../SavedSearches'; +import { useSavedSearches } from '../SavedSearches/useSavedSearches.ts'; import { InboxSkeleton } from './InboxSkeleton.tsx'; import { filterDialogs, getFilterBarSettings } from './filters.ts'; import styles from './inbox.module.css'; @@ -57,12 +57,11 @@ export const Inbox = ({ viewType }: InboxProps) => { const location = useLocation(); const { selectedItems, setSelectedItems, selectedItemCount, inSelectionMode } = useSelectedDialogs(); const { openSnackbar } = useSnackbar(); - const [isSavingSearch, setIsSavingSearch] = useState(false); - const { selectedParties } = useParties(); + const { selectedParties, selectedPartyIds } = useParties(); const { enteredSearchValue } = useSearchString(); const [initialFilters, setInitialFilters] = useState([]); const [activeFilters, setActiveFilters] = useState([]); - + const { saveSearch } = useSavedSearches(selectedPartyIds); const { searchResults, isFetching: isFetchingSearchResults } = useSearchDialogs({ parties: selectedParties, searchValue: enteredSearchValue, @@ -103,8 +102,8 @@ export const Inbox = ({ viewType }: InboxProps) => { (d) => new Date(d.createdAt).getFullYear() === new Date().getFullYear(), ); - const areNotInInbox = itemsToDisplay.every((d) => ['drafts', 'sent', 'bin', 'archive'].includes(getViewType(d))); - if (!shouldShowSearchResults && areNotInInbox) { + 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 }), @@ -148,14 +147,6 @@ export const Inbox = ({ viewType }: InboxProps) => { const savedSearchDisabled = !activeFilters?.length && !enteredSearchValue; const showFilterButton = filterBarSettings.length > 0; - const saveSearchHandler = () => - handleSaveSearch({ - activeFilters, - selectedParties, - enteredSearchValue, - viewType, - setIsSavingSearch, - }); if (isFetchingSearchResults) { return ( @@ -203,10 +194,9 @@ export const Inbox = ({ viewType }: InboxProps) => { resultsCount={itemsToDisplay.length} /> @@ -219,7 +209,9 @@ export const Inbox = ({ viewType }: InboxProps) => { } : undefined } - onSaveBtnClick={saveSearchHandler} + onSaveBtnClick={() => + saveSearch({ filters: activeFilters, selectedParties: selectedPartyIds, enteredSearchValue, viewType }) + } hideSaveButton={savedSearchDisabled} /> diff --git a/packages/frontend/src/pages/SavedSearches/SavedSearchesPage.tsx b/packages/frontend/src/pages/SavedSearches/SavedSearchesPage.tsx index 61f59dc2..5449d18b 100644 --- a/packages/frontend/src/pages/SavedSearches/SavedSearchesPage.tsx +++ b/packages/frontend/src/pages/SavedSearches/SavedSearchesPage.tsx @@ -1,19 +1,9 @@ -import { useQueryClient } from '@tanstack/react-query'; -import type { - PartyFieldsFragment, - SavedSearchData, - SavedSearchesFieldsFragment, - SearchDataValueFilter, -} from 'bff-types-generated'; -import { type Dispatch, type SetStateAction, useRef, useState } from 'react'; +import type { SavedSearchesFieldsFragment } from 'bff-types-generated'; +import { useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { createSavedSearch } from '../../api/queries.ts'; -import type { InboxViewType } from '../../api/useDialogs.tsx'; import { useParties } from '../../api/useParties.ts'; -import { type Filter, PartyDropdown, useSnackbar } from '../../components'; -import { QUERY_KEYS } from '../../constants/queryKeys.ts'; +import { PartyDropdown } from '../../components'; import { useFormatDistance } from '../../i18n/useDateFnsLocale.tsx'; -import { Routes } from '../Inbox/Inbox.tsx'; import { ConfirmDeleteDialog, type DeleteSearchDialogRef } from './ConfirmDeleteDialog/ConfirmDeleteDialog.tsx'; import { EditSavedSearchDialog, @@ -26,49 +16,6 @@ import styles from './savedSearchesPage.module.css'; import { autoFormatRelativeTime, getMostRecentSearchDate } from './searchUtils.ts'; import { useSavedSearches } from './useSavedSearches.ts'; -interface HandleSaveSearchProps { - activeFilters: Filter[]; - selectedParties: PartyFieldsFragment[]; - enteredSearchValue: string; - viewType: InboxViewType; - setIsSavingSearch: Dispatch>; -} - -export const handleSaveSearch = async ({ - activeFilters, - selectedParties, - enteredSearchValue, - viewType, - setIsSavingSearch, -}: HandleSaveSearchProps): Promise => { - const { t } = useTranslation(); - const { openSnackbar } = useSnackbar(); - const queryClient = useQueryClient(); - try { - const data: SavedSearchData = { - filters: activeFilters as SearchDataValueFilter[], - urn: selectedParties.map((party) => party.party) as string[], - searchString: enteredSearchValue, - fromView: Routes[viewType], - }; - setIsSavingSearch(true); - await createSavedSearch('', data); - openSnackbar({ - message: t('savedSearches.saved_success'), - variant: 'success', - }); - } catch (error) { - openSnackbar({ - message: t('savedSearches.saved_error'), - variant: 'error', - }); - console.error('Error creating saved search: ', error); - } finally { - setIsSavingSearch(false); - void queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.SAVED_SEARCHES] }); - } -}; - export const SavedSearchesPage = () => { const [selectedSavedSearch, setSelectedSavedSearch] = useState(null); const [selectedDeleteItem, setSelectedDeleteItem] = useState(undefined); diff --git a/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts b/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts index 470c3815..793d7ddf 100644 --- a/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts +++ b/packages/frontend/src/pages/SavedSearches/useSavedSearches.ts @@ -1,13 +1,33 @@ -import { useQuery } from '@tanstack/react-query'; -import type { SavedSearchesFieldsFragment, SavedSearchesQuery } from 'bff-types-generated'; -import { fetchSavedSearches } from '../../api/queries.ts'; +import { useQuery, useQueryClient } from '@tanstack/react-query'; +import type { + SavedSearchData, + SavedSearchesFieldsFragment, + SavedSearchesQuery, + SearchDataValueFilter, +} from 'bff-types-generated'; +import { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { createSavedSearch, deleteSavedSearch, fetchSavedSearches } from '../../api/queries.ts'; +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'; interface UseSavedSearchesOutput { savedSearches: SavedSearchesFieldsFragment[]; isSuccess: boolean; + isCTALoading: boolean; isLoading: boolean; currentPartySavedSearches: SavedSearchesFieldsFragment[] | undefined; + saveSearch: (props: HandleSaveSearchProps) => Promise; + deleteSearch: (savedSearchId: number) => Promise; +} + +interface HandleSaveSearchProps { + filters: Filter[]; + selectedParties: string[]; + enteredSearchValue: string; + viewType: InboxViewType; } export const filterSavedSearches = ( @@ -32,13 +52,79 @@ export const filterSavedSearches = ( }; export const useSavedSearches = (selectedPartyIds?: string[]): UseSavedSearchesOutput => { + const [isCTALoading, setIsCTALoading] = useState(false); + const { t } = useTranslation(); + const queryClient = useQueryClient(); + const { openSnackbar } = useSnackbar(); + const { data, isLoading, isSuccess } = useQuery({ queryKey: [QUERY_KEYS.SAVED_SEARCHES, selectedPartyIds], queryFn: fetchSavedSearches, retry: 3, staleTime: 1000 * 60 * 20, }); + const savedSearchesUnfiltered = data?.savedSearches as SavedSearchesFieldsFragment[]; const currentPartySavedSearches = filterSavedSearches(savedSearchesUnfiltered, selectedPartyIds || []); - return { savedSearches: savedSearchesUnfiltered, isLoading, isSuccess, currentPartySavedSearches }; + + const saveSearch = async ({ + filters, + selectedParties, + enteredSearchValue, + viewType, + }: HandleSaveSearchProps): Promise => { + try { + setIsCTALoading(true); + const data: SavedSearchData = { + filters: filters as SearchDataValueFilter[], + urn: selectedParties, + searchString: enteredSearchValue, + fromView: Routes[viewType], + }; + await createSavedSearch('', data); + openSnackbar({ + message: t('savedSearches.saved_success'), + variant: 'success', + }); + } catch (error) { + openSnackbar({ + message: t('savedSearches.saved_error'), + variant: 'error', + }); + console.error('Error creating saved search: ', error); + } finally { + void queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.SAVED_SEARCHES] }); + setIsCTALoading(false); + } + }; + + const deleteSearch = async (savedSearchId: number) => { + setIsCTALoading(true); + try { + await deleteSavedSearch(savedSearchId); + openSnackbar({ + message: t('savedSearches.deleted_success'), + variant: 'success', + }); + await queryClient.invalidateQueries({ queryKey: [QUERY_KEYS.SAVED_SEARCHES] }); + } catch (error) { + console.error('Failed to delete saved search:', error); + openSnackbar({ + message: t('savedSearches.delete_failed'), + variant: 'error', + }); + } finally { + setIsCTALoading(false); + } + }; + + return { + savedSearches: savedSearchesUnfiltered, + isLoading, + isSuccess, + currentPartySavedSearches, + isCTALoading, + saveSearch, + deleteSearch, + }; };