diff --git a/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.test.tsx b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.test.tsx new file mode 100644 index 0000000000..b9c736bf61 --- /dev/null +++ b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.test.tsx @@ -0,0 +1,76 @@ +import type { FC } from 'react'; +import { render, screen } from '@testing-library/react'; +import '@testing-library/jest-dom'; +import { testServerRoute, testServerSetup } from 'utils/testServer'; +import { useChangeRequestSearch } from './useChangeRequestSearch.ts'; +import { useSWRConfig } from 'swr'; + +const server = testServerSetup(); + +const TestComponent: FC<{ params: { createdBy?: string; limit?: number } }> = ({ + params, +}) => { + const { loading, error, changeRequests, total, refetch } = + useChangeRequestSearch(params); + const { cache } = useSWRConfig(); + if (loading) { + return
Loading...
; + } + + if (error) { + return
Error: {error}
; + } + + return ( +
+ +
+ Change Requests:{' '} + {changeRequests.map((cr) => cr.title).join(', ')} +
+
Total: {total}
+
Cache: {[...cache.keys()]}
+
+ ); +}; + +describe('useChangeRequestSearch', () => { + test('should overwrite cache total with 0 if the next result has 0 values', async () => { + const createdBy = '789'; + const url = `/api/admin/search/change-requests?createdBy=${createdBy}`; + testServerRoute(server, url, { + changeRequests: [ + { + id: 1, + title: 'Change Request 1', + createdAt: '2024-01-01T00:00:00Z', + createdBy: { id: 789, username: 'testuser' }, + environment: 'production', + project: 'test-project', + features: ['feature1'], + segments: [], + state: 'Draft', + }, + ], + total: 1, + }); + + const { rerender } = render(); + + await screen.findByText(/Total: 1/); + + testServerRoute(server, url, { + changeRequests: [], + total: 0, + }); + + // force fetch + const button = await screen.findByRole('button', { name: 'refetch' }); + button.click(); + + rerender(); + await screen.findByText(/Total: 0/); + }); +}); diff --git a/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts new file mode 100644 index 0000000000..fca192d487 --- /dev/null +++ b/frontend/src/hooks/api/getters/useChangeRequestSearch/useChangeRequestSearch.ts @@ -0,0 +1,127 @@ +import useSWR, { type SWRConfiguration } from 'swr'; +import { useCallback, useEffect } from 'react'; +import { formatApiPath } from 'utils/formatPath'; +import handleErrorResponses from '../httpErrorResponseHandler.js'; +import type { + SearchChangeRequestsParams, + ChangeRequestSearchResponseSchema, +} from 'openapi'; +import { useClearSWRCache } from 'hooks/useClearSWRCache'; + +type UseChangeRequestSearchOutput = { + loading: boolean; + initialLoad: boolean; + error: string; + refetch: () => void; +} & ChangeRequestSearchResponseSchema; + +type CacheValue = { + total: number; + initialLoad: boolean; + [key: string]: number | boolean; +}; + +type InternalCache = Record; + +const fallbackData: ChangeRequestSearchResponseSchema = { + changeRequests: [], + total: 0, +}; + +const SWR_CACHE_SIZE = 10; +const PATH = 'api/admin/search/change-requests?'; + +const createChangeRequestSearch = () => { + const internalCache: InternalCache = {}; + + const initCache = (id: string) => { + internalCache[id] = { + total: 0, + initialLoad: true, + }; + }; + + const set = (id: string, key: string, value: number | boolean) => { + if (!internalCache[id]) { + initCache(id); + } + internalCache[id][key] = value; + }; + + const get = (id: string) => { + if (!internalCache[id]) { + initCache(id); + } + return internalCache[id]; + }; + + return ( + params: SearchChangeRequestsParams, + options: SWRConfiguration = {}, + cachePrefix: string = '', + ): UseChangeRequestSearchOutput => { + const { KEY, fetcher } = getChangeRequestSearchFetcher(params); + const swrKey = `${cachePrefix}${KEY}`; + const cacheId = 'global'; + useClearSWRCache(swrKey, PATH, SWR_CACHE_SIZE); + + useEffect(() => { + initCache(cacheId); + }, []); + + const { data, error, mutate, isLoading } = + useSWR(swrKey, fetcher, options); + + const refetch = useCallback(() => { + mutate(); + }, [mutate]); + + const cacheValues = get(cacheId); + + if (data?.total !== undefined) { + set(cacheId, 'total', data.total); + } + + if (!isLoading && cacheValues.initialLoad) { + set(cacheId, 'initialLoad', false); + } + + const returnData = data || fallbackData; + return { + ...returnData, + loading: isLoading, + error, + refetch, + total: cacheValues.total, + initialLoad: isLoading && cacheValues.initialLoad, + }; + }; +}; + +export const DEFAULT_PAGE_LIMIT = 25; + +const getChangeRequestSearchFetcher = (params: SearchChangeRequestsParams) => { + const urlSearchParams = new URLSearchParams( + Array.from( + Object.entries(params) + .filter(([_, value]) => !!value) + .map(([key, value]) => [key, value.toString()]), + ), + ).toString(); + const KEY = `${PATH}${urlSearchParams}`; + const fetcher = () => { + const path = formatApiPath(KEY); + return fetch(path, { + method: 'GET', + }) + .then(handleErrorResponses('Change request search')) + .then((res) => res.json()); + }; + + return { + fetcher, + KEY, + }; +}; + +export const useChangeRequestSearch = createChangeRequestSearch(); diff --git a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts index 15b0bc232f..8c45e49612 100644 --- a/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts +++ b/frontend/src/hooks/api/getters/useFeatureSearch/useFeatureSearch.ts @@ -63,7 +63,7 @@ const createFeatureSearch = () => { useClearSWRCache(swrKey, PATH, SWR_CACHE_SIZE); useEffect(() => { - initCache(params.project || ''); + initCache(cacheId); }, []); const { data, error, mutate, isLoading } = useSWR( diff --git a/frontend/src/openapi/models/searchChangeRequestsParams.ts b/frontend/src/openapi/models/searchChangeRequestsParams.ts index 35d124d6f8..9dec47a76f 100644 --- a/frontend/src/openapi/models/searchChangeRequestsParams.ts +++ b/frontend/src/openapi/models/searchChangeRequestsParams.ts @@ -16,9 +16,9 @@ export type SearchChangeRequestsParams = { /** * The number of change requests to skip when returning a page. By default it is set to 0. */ - offset?: string; + offset?: number; /** * The number of change requests to return in a page. By default it is set to 50. The maximum is 1000. */ - limit?: string; + limit?: number; };