diff --git a/eslint.config.js b/eslint.config.js index 77868b52f..1739a04c7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,4 +1,5 @@ import eslint from '@eslint/js'; +import queryPlugin from '@tanstack/eslint-plugin-query'; import prettierPlugin from 'eslint-config-prettier/flat'; import cypressPlugin from 'eslint-plugin-cypress'; import jsxA11yPlugin from 'eslint-plugin-jsx-a11y'; @@ -17,6 +18,7 @@ export default defineConfig( reactPlugin.configs.flat['jsx-runtime'], reactHooksPlugin.configs.flat.recommended, jsxA11yPlugin.flatConfigs.recommended, + queryPlugin.configs['flat/recommended'], // See https://github.com/prettier/eslint-config-prettier put last prettierPlugin, { diff --git a/packages/datagateway-common/package.json b/packages/datagateway-common/package.json index 66e75a1f0..e8ea645a4 100644 --- a/packages/datagateway-common/package.json +++ b/packages/datagateway-common/package.json @@ -13,8 +13,8 @@ "@emotion/styled": "11.14.1", "@mui/lab": "5.0.0-alpha.177", "@mui/x-date-pickers": "6.20.2", - "@tanstack/react-query": "4.43.0", - "@tanstack/react-query-devtools": "4.43.0", + "@tanstack/react-query": "5.90.21", + "@tanstack/react-query-devtools": "5.91.3", "@types/lodash.debounce": "4.0.6", "@vitejs/plugin-react": "5.2.0", "axios": "1.13.5", @@ -48,6 +48,7 @@ "devDependencies": { "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", + "@tanstack/eslint-plugin-query": "5.91.4", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", "@testing-library/react": "16.3.2", diff --git a/packages/datagateway-common/src/api/cart.test.ts b/packages/datagateway-common/src/api/cart.test.ts index ae7397cc8..b0f934dd0 100644 --- a/packages/datagateway-common/src/api/cart.test.ts +++ b/packages/datagateway-common/src/api/cart.test.ts @@ -13,14 +13,15 @@ import { useSubmitCart, } from '.'; import { DownloadCart } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; -vi.mock('../handleICATError'); - describe('Cart api functions', () => { let mockData: DownloadCart; const getElementByIdSpy = vi.spyOn(document, 'getElementById'); + const handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); beforeEach(() => { mockData = { @@ -47,7 +48,7 @@ describe('Cart api functions', () => { vi.mocked(axios.get).mockClear(); vi.mocked(axios.post).mockClear(); vi.mocked(axios.delete).mockClear(); - vi.mocked(handleICATError).mockClear(); + vi.mocked(handleICATErrorSpy).mockClear(); vi.mocked(getElementByIdSpy).mockClear(); }); @@ -81,7 +82,7 @@ describe('Cart api functions', () => { wrapper: createReactQueryWrapper(), }); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); expect(axios.get).not.toHaveBeenCalled(); @@ -98,9 +99,12 @@ describe('Cart api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error message', + }, + undefined + ); }); }); @@ -158,8 +162,8 @@ describe('Cart api functions', () => { }); expect(result.current.failureCount).toBe(2); - expect(handleICATError).toHaveBeenCalledTimes(1); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledTimes(1); + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'Test error message', }); }); @@ -221,8 +225,8 @@ describe('Cart api functions', () => { }); expect(result.current.failureCount).toBe(2); - expect(handleICATError).toHaveBeenCalledTimes(1); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledTimes(1); + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'Test error message', }); }); @@ -295,7 +299,7 @@ describe('Cart api functions', () => { expect(result.current.useSubmitCart.isError).toBe(true) ); - expect(handleICATError).toHaveBeenCalledWith( + expect(handleICATErrorSpy).toHaveBeenCalledWith( expect.objectContaining({ message: 'No downloadId returned from submitCart request', }) @@ -331,7 +335,7 @@ describe('Cart api functions', () => { expect(result.current.useSubmitCart.isError).toBe(true) ); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'test error message', }); }); @@ -389,9 +393,12 @@ describe('Cart api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error message', + }, + undefined + ); }); }); @@ -466,9 +473,12 @@ describe('Cart api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error message', + }, + undefined + ); }); }); @@ -507,9 +517,12 @@ describe('Cart api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error message', + }, + undefined + ); }); }); @@ -575,7 +588,7 @@ describe('Cart api functions', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'test error message', }); }); @@ -646,7 +659,7 @@ describe('Cart api functions', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'test error message', }); }); diff --git a/packages/datagateway-common/src/api/cart.ts b/packages/datagateway-common/src/api/cart.ts index 817bef504..de56fa7c1 100644 --- a/packages/datagateway-common/src/api/cart.ts +++ b/packages/datagateway-common/src/api/cart.ts @@ -1,12 +1,4 @@ -import { - UseMutationOptions, - UseMutationResult, - UseQueryOptions, - UseQueryResult, - useMutation, - useQuery, - useQueryClient, -} from '@tanstack/react-query'; +import { UseQueryResult, useMutation, useQuery } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { format } from 'date-fns'; import { useTranslation } from 'react-i18next'; @@ -60,7 +52,7 @@ const addOrRemoveFromCart = ( .then((response) => response.data.cartItems); }; -export const useCart = (): UseQueryResult => { +export const useCart = () => { const downloadApiUrl = useSelector( (state: StateType) => state.dgcommon.urls.downloadApiUrl ); @@ -68,30 +60,31 @@ export const useCart = (): UseQueryResult => { (state: StateType) => state.dgcommon.facilityName ); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['cart'], - () => + return useQuery({ + queryKey: ['cart', facilityName, downloadApiUrl], + + queryFn: () => fetchDownloadCart({ facilityName, downloadApiUrl, }), - { - enabled: - document.getElementById('datagateway-dataview') !== null || - document.getElementById('datagateway-search') !== null, - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - staleTime: 0, - } - ); + + enabled: + document.getElementById('datagateway-dataview') !== null || + document.getElementById('datagateway-search') !== null, + + meta: { + icatError: true, + }, + + retry: retryICATErrors, + staleTime: 0, + }); }; export const useAddToCart = ( entityType: 'investigation' | 'dataset' | 'datafile' -): UseMutationResult => { - const queryClient = useQueryClient(); +) => { const downloadApiUrl = useSelector( (state: StateType) => state.dgcommon.urls.downloadApiUrl ); @@ -99,35 +92,35 @@ export const useAddToCart = ( (state: StateType) => state.dgcommon.facilityName ); - return useMutation( - (entityIds: number[]) => + return useMutation({ + mutationFn: (entityIds: number[]) => addOrRemoveFromCart(entityType, entityIds, { facilityName, downloadApiUrl, }), - { - onSuccess: (data) => { - queryClient.setQueryData(['cart'], data); - }, - retry: (failureCount, error) => { - // if we get 431 we know this is an intermittent error so retry - if (error.response?.status === 431 && failureCount < 3) { - return true; - } else { - return false; - } - }, - onError: (error) => { - handleICATError(error); - }, - } - ); + + onSuccess: (data, _variables, _onMutateResult, context) => { + context.client.setQueriesData({ queryKey: ['cart'] }, data); + }, + + retry: (failureCount, error) => { + // if we get 431 we know this is an intermittent error so retry + if (error.response?.status === 431 && failureCount < 3) { + return true; + } else { + return false; + } + }, + + onError: (error: AxiosError) => { + handleICATError(error); + }, + }); }; export const useRemoveFromCart = ( entityType: 'investigation' | 'dataset' | 'datafile' -): UseMutationResult => { - const queryClient = useQueryClient(); +) => { const downloadApiUrl = useSelector( (state: StateType) => state.dgcommon.urls.downloadApiUrl ); @@ -135,8 +128,8 @@ export const useRemoveFromCart = ( (state: StateType) => state.dgcommon.facilityName ); - return useMutation( - (entityIds: number[]) => + return useMutation({ + mutationFn: (entityIds: number[]) => addOrRemoveFromCart( entityType, entityIds, @@ -146,23 +139,24 @@ export const useRemoveFromCart = ( }, true ), - { - onSuccess: (data) => { - queryClient.setQueryData(['cart'], data); - }, - retry: (failureCount, error) => { - // if we get 431 we know this is an intermittent error so retry - if (error.response?.status === 431 && failureCount < 3) { - return true; - } else { - return false; - } - }, - onError: (error) => { - handleICATError(error); - }, - } - ); + + onSuccess: (data, _variables, _onMutateResult, context) => { + context.client.setQueriesData({ queryKey: ['cart'] }, data); + }, + + retry: (failureCount, error) => { + // if we get 431 we know this is an intermittent error so retry + if (error.response?.status === 431 && failureCount < 3) { + return true; + } else { + return false; + } + }, + + onError: (error: AxiosError) => { + handleICATError(error); + }, + }); }; export const getDownloadTypes: ( @@ -188,19 +182,17 @@ export const getDownloadTypes: ( export const useDownloadTypes = ( facilityName: string, downloadApiUrl: string -): UseQueryResult => { +) => { const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['downloadtypes'], - () => getDownloadTypes(facilityName, downloadApiUrl), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + return useQuery({ + queryKey: ['downloadtypes', facilityName, downloadApiUrl], + queryFn: () => getDownloadTypes(facilityName, downloadApiUrl), + + meta: { icatError: true }, + + retry: retryICATErrors, + }); }; export type SubmitCartZipType = 'ZIP' | 'ZIP_AND_COMPRESS'; @@ -284,12 +276,6 @@ export const getDefaultFileName = ( return defaultName; }; -/** - * Defines the function that when called will roll back any optimistic changes - * performed during a mutation. - */ -type RollbackFunction = () => void; - export interface SubmitCartParams { transport: string; emailAddress: string; @@ -302,26 +288,16 @@ export interface SubmitCartParams { * Returns the download id for the submitted cart, which can then be used * to query more info. */ -export const useSubmitCart = ( - facilityName: string, - downloadApiUrl: string, - options?: UseMutationOptions< - number, - AxiosError, - SubmitCartParams, - RollbackFunction - > -): UseMutationResult< - number, - AxiosError, - SubmitCartParams, - RollbackFunction -> => { - const queryClient = useQueryClient(); +export const useSubmitCart = (facilityName: string, downloadApiUrl: string) => { const [t] = useTranslation(); - return useMutation( - ({ transport, emailAddress, fileName: userFileName, zipType }) => { + return useMutation({ + mutationFn: ({ + transport, + emailAddress, + fileName: userFileName, + zipType, + }: SubmitCartParams) => { let fileName = userFileName; if (!fileName) { fileName = getDefaultFileName( @@ -338,19 +314,17 @@ export const useSubmitCart = ( zipType ); }, - { - onError: (error, _, rollback) => { - handleICATError(error); - if (rollback) rollback(); - }, - onSettled: () => { - queryClient.invalidateQueries(['cart']); - }, + onError: (error: AxiosError) => { + handleICATError(error); + }, - ...(options ?? {}), - } - ); + onSettled: (_data, _error, _variables, _onMutateResult, context) => { + context.client.invalidateQueries({ + queryKey: ['cart'], + }); + }, + }); }; export const getDownload: ( @@ -377,7 +351,6 @@ export interface UseDownloadParams { /** * A React hook that fetches a single download with the given id. - * useQuery options can be passed in, which will override the default used. * * Example: * ``` @@ -391,28 +364,20 @@ export const useDownload = ({ id, facilityName, downloadApiUrl, - ...queryOptions -}: UseDownloadParams & - UseQueryOptions< - Download, - AxiosError, - Download, - ['download', number] - >): UseQueryResult => { + enabled, +}: UseDownloadParams & { enabled?: boolean }) => { // Load the download settings for use. const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['download', id], - () => getDownload(id, facilityName, downloadApiUrl), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - ...queryOptions, - } - ); + return useQuery({ + queryKey: ['download', id, facilityName, downloadApiUrl], + queryFn: () => getDownload(id, facilityName, downloadApiUrl), + + meta: { icatError: true }, + + retry: retryICATErrors, + enabled, + }); }; export const fetchQueueAllowed = (config: { @@ -439,21 +404,25 @@ export const useQueueAllowed = (): UseQueryResult => { (state: StateType) => state.dgcommon.facilityName ); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['isQueueAllowed', readSciGatewayToken().sessionId], // put session id in here to ensure we refresh if user logs out and logs in as new user - () => + return useQuery({ + // put session id in query key to ensure we refresh if user logs out and logs in as new user + queryKey: [ + 'isQueueAllowed', + readSciGatewayToken().sessionId, + facilityName, + downloadApiUrl, + ], + queryFn: () => fetchQueueAllowed({ facilityName, downloadApiUrl, }), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - staleTime: Infinity, - } - ); + + meta: { icatError: true }, + + retry: retryICATErrors, + staleTime: Infinity, + }); }; export const queueVisit: ( @@ -488,8 +457,10 @@ export const queueVisit: ( }); }; -export interface QueueVisitParams - extends Pick { +export interface QueueVisitParams extends Pick< + SubmitCartParams, + 'emailAddress' | 'transport' | 'fileName' +> { entityId: string; } @@ -498,23 +469,14 @@ export interface QueueVisitParams * Returns the list of download ids for the submitted visit, * which can then be used to query more info. */ -export const useQueueVisit = ( - facilityName: string, - downloadApiUrl: string, - options?: UseMutationOptions< - string[], - AxiosError, - QueueVisitParams, - RollbackFunction - > -): UseMutationResult< - string[], - AxiosError, - QueueVisitParams, - RollbackFunction -> => { - return useMutation( - ({ transport, emailAddress, fileName, entityId }) => +export const useQueueVisit = (facilityName: string, downloadApiUrl: string) => { + return useMutation({ + mutationFn: ({ + transport, + emailAddress, + fileName, + entityId, + }: QueueVisitParams) => queueVisit( entityId, transport, @@ -523,14 +485,11 @@ export const useQueueVisit = ( facilityName, downloadApiUrl ), - { - onError: (error, _, rollback) => { - handleICATError(error); - if (rollback) rollback(); - }, - ...(options ?? {}), - } - ); + + onError: (error: AxiosError) => { + handleICATError(error); + }, + }); }; export const queueDataCollection: ( @@ -565,8 +524,10 @@ export const queueDataCollection: ( }); }; -export interface QueueDataCollectionParams - extends Pick { +export interface QueueDataCollectionParams extends Pick< + SubmitCartParams, + 'emailAddress' | 'transport' | 'fileName' +> { entityId: string; } @@ -577,21 +538,15 @@ export interface QueueDataCollectionParams */ export const useQueueDataCollection = ( facilityName: string, - downloadApiUrl: string, - options?: UseMutationOptions< - string[], - AxiosError, - QueueVisitParams, - RollbackFunction - > -): UseMutationResult< - string[], - AxiosError, - QueueVisitParams, - RollbackFunction -> => { - return useMutation( - ({ transport, emailAddress, fileName, entityId }) => + downloadApiUrl: string +) => { + return useMutation({ + mutationFn: ({ + transport, + emailAddress, + fileName, + entityId, + }: QueueVisitParams) => queueDataCollection( entityId, transport, @@ -600,12 +555,9 @@ export const useQueueDataCollection = ( facilityName, downloadApiUrl ), - { - onError: (error, _, rollback) => { - handleICATError(error); - if (rollback) rollback(); - }, - ...(options ?? {}), - } - ); + + onError: (error: AxiosError) => { + handleICATError(error); + }, + }); }; diff --git a/packages/datagateway-common/src/api/dataPublications.test.tsx b/packages/datagateway-common/src/api/dataPublications.test.tsx index aeb3ac5cd..5c6069580 100644 --- a/packages/datagateway-common/src/api/dataPublications.test.tsx +++ b/packages/datagateway-common/src/api/dataPublications.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios from 'axios'; import { History, createMemoryHistory } from 'history'; import { DataPublication } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; import { useDataPublication, @@ -14,12 +14,14 @@ import { useDataPublicationsPaginated, } from './dataPublications'; -vi.mock('../handleICATError'); - describe('data publications api functions', () => { let mockData: DataPublication[] = []; let history: History; let params: URLSearchParams; + const handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); + beforeEach(() => { mockData = [ { @@ -42,7 +44,7 @@ describe('data publications api functions', () => { }); afterEach(() => { - vi.mocked(handleICATError).mockClear(); + vi.mocked(handleICATErrorSpy).mockClear(); vi.mocked(axios.get).mockClear(); }); @@ -116,7 +118,7 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2); }); - it('sends axios request to fetch paginated data publications and calls handleICATError on failure', async () => { + it('sends axios request to fetch paginated data publications and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -139,7 +141,12 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -203,9 +210,7 @@ describe('data publications api functions', () => { ); expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - await result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + await result.current.fetchNextPage(); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -239,7 +244,7 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(3); }); - it('sends axios request to fetch infinite data publications and calls handleICATError on failure', async () => { + it('sends axios request to fetch infinite data publications and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -262,7 +267,12 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -324,7 +334,7 @@ describe('data publications api functions', () => { expect(result.current.data).toEqual(mockData[0]); }); - it('sends axios request to fetch a single data publication and calls handleICATError on failure', async () => { + it('sends axios request to fetch a single data publication and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -377,7 +387,12 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -422,7 +437,7 @@ describe('data publications api functions', () => { expect(result.current.data).toEqual(mockData); }); - it('sends axios request to fetch a single data publication and calls handleICATError on failure', async () => { + it('sends axios request to fetch a single data publication and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -454,7 +469,12 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -512,7 +532,7 @@ describe('data publications api functions', () => { expect(result.current.data).toEqual(mockData.length); }); - it('sends axios request to fetch data publication count and calls handleICATError on failure', async () => { + it('sends axios request to fetch data publication count and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -531,7 +551,12 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -589,9 +614,7 @@ describe('data publications api functions', () => { ); expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + result.current.fetchNextPage(); await waitFor(() => result.current.isFetching); @@ -708,7 +731,7 @@ describe('data publications api functions', () => { expect(result.current.data?.pages).toStrictEqual([mockData[0]]); }); - it("sends axios request to fetch a data publication's content and calls handleICATError on failure", async () => { + it("sends axios request to fetch a data publication's content and calls handleICATErrorSpy on failure", async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -742,7 +765,12 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -870,7 +898,7 @@ describe('data publications api functions', () => { expect(result.current.data).toEqual(mockData.length); }); - it('sends axios request to fetch data publication datafile count and calls handleICATError on failure', async () => { + it('sends axios request to fetch data publication datafile count and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -901,7 +929,12 @@ describe('data publications api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); }); diff --git a/packages/datagateway-common/src/api/dataPublications.tsx b/packages/datagateway-common/src/api/dataPublications.tsx index e37eca750..26916a3c2 100644 --- a/packages/datagateway-common/src/api/dataPublications.tsx +++ b/packages/datagateway-common/src/api/dataPublications.tsx @@ -1,14 +1,7 @@ -import { - UseInfiniteQueryResult, - UseQueryOptions, - UseQueryResult, - useInfiniteQuery, - useQuery, -} from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { IndexRange } from 'react-virtualized'; import { AdditionalFilters, DataPublication, @@ -16,11 +9,12 @@ import { Dataset, FiltersType, Investigation, + SkipAndLimitType, SortType, } from '../app.types'; -import handleICATError from '../handleICATError'; import { readSciGatewayToken } from '../parseTokens'; import { StateType } from '../state/app.types'; +import { INFINITE_SCROLL_BATCH_SIZE } from '../table/table.component'; import { fetchDatafileCountQuery, fetchDatafiles } from './datafiles'; import { fetchDatasetCountQuery, fetchDatasets } from './datasets'; import { getApiParams, parseSearchToQuery } from './index'; @@ -34,16 +28,13 @@ const fetchDataPublications = ( filters: FiltersType; }, additionalFilters?: AdditionalFilters, - offsetParams?: IndexRange + skipAndLimit?: SkipAndLimitType ): Promise => { const params = getApiParams(sortAndFilters); - if (offsetParams) { - params.append('skip', JSON.stringify(offsetParams.startIndex)); - params.append( - 'limit', - JSON.stringify(offsetParams.stopIndex - offsetParams.startIndex + 1) - ); + if (skipAndLimit) { + params.append('skip', JSON.stringify(skipAndLimit.skip)); + params.append('limit', JSON.stringify(skipAndLimit.limit)); } if (additionalFilters) { @@ -72,28 +63,14 @@ const fetchDataPublications = ( export const useDataPublicationsPaginated = ( additionalFilters?: AdditionalFilters, isMounted?: boolean -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort, page, results } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - DataPublication[], - AxiosError, - DataPublication[], - [ - string, - { - sort: string; - filters: FiltersType; - page: number; - results: number; - }, - AdditionalFilters? - ] - >( - [ + return useQuery({ + queryKey: [ 'dataPublication', { sort: JSON.stringify(sort), // need to stringify sort as property order is important! @@ -102,80 +79,73 @@ export const useDataPublicationsPaginated = ( results: results ?? 10, }, additionalFilters, - ], - (params) => { + apiUrl, + ] as const, + + queryFn: (params) => { const { page, results } = params.queryKey[1]; - const startIndex = (page - 1) * results; - const stopIndex = startIndex + results - 1; + const skip = (page - 1) * results; + const limit = results; return fetchDataPublications( apiUrl, { sort, filters }, additionalFilters, { - startIndex, - stopIndex, + skip, + limit, } ); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const useDataPublicationsInfinite = ( additionalFilters?: AdditionalFilters, isMounted?: boolean -): UseInfiniteQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery( - [ + return useInfiniteQuery({ + queryKey: [ 'dataPublication', { sort: JSON.stringify(sort), filters }, // need to stringify sort as property order is important! additionalFilters, + apiUrl, ], - (params) => { - const offsetParams = params.pageParam ?? { startIndex: 0, stopIndex: 49 }; - return fetchDataPublications( + queryFn: (params) => + fetchDataPublications( apiUrl, { sort, filters }, additionalFilters, - offsetParams - ); - }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + params.pageParam + ), + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const useDataPublication = ( dataPublicationId?: number, - queryOptions?: UseQueryOptions< - DataPublication[], - AxiosError, - DataPublication, - [string, number] - > -): UseQueryResult => { + enabled?: boolean +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['dataPublication', dataPublicationId ?? -1], - () => { + return useQuery({ + queryKey: ['dataPublication', dataPublicationId ?? -1, apiUrl], + queryFn: () => { return fetchDataPublications(apiUrl, { sort: {}, filters: {} }, [ { filterType: 'where', @@ -211,76 +181,53 @@ export const useDataPublication = ( }, ]); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: typeof dataPublicationId !== 'undefined', - select: (data) => data[0], - ...queryOptions, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled: enabled ?? typeof dataPublicationId !== 'undefined', + select: (data) => data[0], + }); }; export const useDataPublicationsByFilters = ( additionalFilters: AdditionalFilters, - queryOptions?: UseQueryOptions< - DataPublication[], - AxiosError, - DataPublication[], - [string, AdditionalFilters] - > -): UseQueryResult => { + enabled?: boolean +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['dataPublication', additionalFilters], - (_params) => { + return useQuery({ + queryKey: ['dataPublication', additionalFilters, apiUrl], + queryFn: (_params) => { return fetchDataPublications( apiUrl, { sort: {}, filters: {} }, additionalFilters ); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - ...queryOptions, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled, + }); }; export const useDataPublicationContent = ( dataPublicationId: string, entityType: 'investigation' | 'dataset' | 'datafile' -): UseInfiniteQueryResult< - (Investigation | Dataset | Datafile)[], - AxiosError -> => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery< - (Investigation | Dataset | Datafile)[], - AxiosError, - (Investigation | Dataset | Datafile)[], - [string, string, string, { sort: SortType; filters: FiltersType }] - >( - [ + return useInfiniteQuery({ + queryKey: [ 'dataPublicationContent', entityType, dataPublicationId, { sort, filters }, - ], - (params) => { + ] as const, + queryFn: (params): Promise => { const { sort, filters } = params.queryKey[3]; - const offsetParams = params.pageParam ?? { startIndex: 0, stopIndex: 49 }; if (entityType === 'investigation') { return fetchInvestigations( apiUrl, @@ -300,7 +247,7 @@ export const useDataPublicationContent = ( }), }, ], - offsetParams + params.pageParam ); } if (entityType === 'dataset') { @@ -317,7 +264,7 @@ export const useDataPublicationContent = ( }), }, ], - offsetParams + params.pageParam ); } if (entityType === 'datafile') { @@ -334,34 +281,40 @@ export const useDataPublicationContent = ( }), }, ], - offsetParams + params.pageParam ); } else { // shouldn't happen - just feels better to explicity check entityType return Promise.reject(); } }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + retry: retryICATErrors, + }); }; export const useDataPublicationContentCount = ( dataPublicationId: string, entityType: 'investigation' | 'dataset' | 'datafile' -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['dataPublicationContentCount', entityType, dataPublicationId, filters], - () => { + return useQuery({ + queryKey: [ + 'dataPublicationContentCount', + entityType, + dataPublicationId, + filters, + ], + queryFn: () => { if (entityType === 'investigation') { return fetchInvestigationCount(apiUrl, filters, [ { @@ -401,13 +354,9 @@ export const useDataPublicationContentCount = ( return Promise.reject(); } }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; const fetchDataPublicationCount = ( @@ -438,28 +387,25 @@ const fetchDataPublicationCount = ( export const useDataPublicationCount = ( additionalFilters?: AdditionalFilters -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - number, - AxiosError, - number, - [string, string, { filters: FiltersType }, AdditionalFilters?] - >( - ['count', 'dataPublication', { filters }, additionalFilters], - (params) => { + return useQuery({ + queryKey: [ + 'count', + 'dataPublication', + { filters }, + additionalFilters, + apiUrl, + ] as const, + queryFn: (params) => { const { filters } = params.queryKey[2]; return fetchDataPublicationCount(apiUrl, filters, additionalFilters); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; diff --git a/packages/datagateway-common/src/api/datafiles.test.tsx b/packages/datagateway-common/src/api/datafiles.test.tsx index 7f2dcb8c1..dd4b57791 100644 --- a/packages/datagateway-common/src/api/datafiles.test.tsx +++ b/packages/datagateway-common/src/api/datafiles.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios from 'axios'; import { History, createMemoryHistory } from 'history'; import { Datafile } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; import { downloadDatafile, @@ -13,12 +13,12 @@ import { useDatafilesPaginated, } from './datafiles'; -vi.mock('../handleICATError'); - describe('datafile api functions', () => { let mockData: Datafile[] = []; let history: History; let params: URLSearchParams; + let handleICATErrorSpy: ReturnType; + beforeEach(() => { mockData = [ { @@ -44,10 +44,12 @@ describe('datafile api functions', () => { ], }); params = new URLSearchParams(); + handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); }); afterEach(() => { - vi.mocked(handleICATError).mockClear(); vi.mocked(axios.get).mockClear(); vi.restoreAllMocks(); }); @@ -139,7 +141,12 @@ describe('datafile api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -197,9 +204,7 @@ describe('datafile api functions', () => { ); expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - await result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + await result.current.fetchNextPage(); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -256,7 +261,12 @@ describe('datafile api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -320,7 +330,12 @@ describe('datafile api functions', () => { expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -365,7 +380,7 @@ describe('datafile api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith(error); + expect(handleICATErrorSpy).toHaveBeenCalledWith(error); }); }); @@ -451,7 +466,12 @@ describe('datafile api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); diff --git a/packages/datagateway-common/src/api/datafiles.tsx b/packages/datagateway-common/src/api/datafiles.tsx index 02a85ebb8..ac4a56e43 100644 --- a/packages/datagateway-common/src/api/datafiles.tsx +++ b/packages/datagateway-common/src/api/datafiles.tsx @@ -1,25 +1,19 @@ -import type { - UseInfiniteQueryResult, - UseQueryOptions, - UseQueryResult, -} from '@tanstack/react-query'; +import type { UseQueryResult } from '@tanstack/react-query'; import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; import axios, { AxiosError, AxiosProgressEvent } from 'axios'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { IndexRange } from 'react-virtualized'; import { getApiParams, parseSearchToQuery, useEntity } from '.'; import { AdditionalFilters, Datafile, - Dataset, FiltersType, - Investigation, + SkipAndLimitType, SortType, } from '../app.types'; -import handleICATError from '../handleICATError'; import { readSciGatewayToken } from '../parseTokens'; import { StateType } from '../state/app.types'; +import { INFINITE_SCROLL_BATCH_SIZE } from '../table/table.component'; import { useRetryICATErrors } from './retryICATErrors'; export const fetchDatafiles = ( @@ -29,16 +23,13 @@ export const fetchDatafiles = ( filters: FiltersType; }, additionalFilters?: AdditionalFilters, - offsetParams?: IndexRange + skipAndLimit?: SkipAndLimitType ): Promise => { const params = getApiParams(sortAndFilters); - if (offsetParams) { - params.append('skip', JSON.stringify(offsetParams.startIndex)); - params.append( - 'limit', - JSON.stringify(offsetParams.stopIndex - offsetParams.startIndex + 1) - ); + if (skipAndLimit) { + params.append('skip', JSON.stringify(skipAndLimit.skip)); + params.append('limit', JSON.stringify(skipAndLimit.limit)); } additionalFilters?.forEach((filter) => { @@ -66,22 +57,8 @@ export const useDatafilesPaginated = ( const { filters, sort, page, results } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - Datafile[], - AxiosError, - Datafile[], - [ - string, - { - sort: string; - filters: FiltersType; - page: number; - results: number; - }, - AdditionalFilters? - ] - >( - [ + return useQuery({ + queryKey: [ 'datafile', { sort: JSON.stringify(sort), // need to stringify sort as property order is important! @@ -90,54 +67,58 @@ export const useDatafilesPaginated = ( results: results ?? 10, }, additionalFilters, - ], - (params) => { + apiUrl, + ] as const, + + queryFn: (params) => { const { page, results } = params.queryKey[1]; - const startIndex = (page - 1) * results; - const stopIndex = startIndex + results - 1; + const skip = (page - 1) * results; + const limit = results; return fetchDatafiles(apiUrl, { sort, filters }, additionalFilters, { - startIndex, - stopIndex, + skip, + limit, }); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + + meta: { icatError: true }, + + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const useDatafilesInfinite = ( additionalFilters?: AdditionalFilters, isMounted?: boolean -): UseInfiniteQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery( - ['datafile', { sort: JSON.stringify(sort), filters }, additionalFilters], // need to stringify sort as property order is important! - (params) => { - const offsetParams = params.pageParam ?? { startIndex: 0, stopIndex: 49 }; - return fetchDatafiles( + return useInfiniteQuery({ + queryKey: [ + 'datafile', + { sort: JSON.stringify(sort), filters }, // need to stringify sort as property order is important! + additionalFilters, + apiUrl, + ], + queryFn: (params) => + fetchDatafiles( apiUrl, { sort, filters }, additionalFilters, - offsetParams - ); - }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + params.pageParam + ), + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const fetchDatafileCountQuery = ( @@ -164,33 +145,28 @@ export const fetchDatafileCountQuery = ( .then((response) => response.data); }; -export const useDatafileCount = ( - additionalFilters?: AdditionalFilters -): UseQueryResult => { +export const useDatafileCount = (additionalFilters?: AdditionalFilters) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const filters = parseSearchToQuery(location.search).filters; const retryICATErrors = useRetryICATErrors(); - return useQuery< - number, - AxiosError, - number, - [string, string, { filters: FiltersType }, AdditionalFilters?] - >( - ['count', 'datafile', { filters }, additionalFilters], - (params) => { + return useQuery({ + queryKey: [ + 'count', + 'datafile', + { filters }, + additionalFilters, + apiUrl, + ] as const, + queryFn: (params) => { const { filters } = params.queryKey[2]; return fetchDatafileCountQuery(apiUrl, filters, additionalFilters); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; export const useDatafileDetails = ( @@ -199,17 +175,14 @@ export const useDatafileDetails = ( filterType: 'include'; filterValue: string; }, - options?: UseQueryOptions + enabled?: boolean ): UseQueryResult => { return useEntity( 'datafile', 'id', datafileId.toString(), includeFilter, - options as UseQueryOptions< - Datafile | Investigation | Dataset, - AxiosError | Error - > + enabled ); }; @@ -251,35 +224,30 @@ const downloadDatafileToMemory = ({ export const useDatafileContent = ({ datafileId, onDownloadProgress, - ...queryOptions + enabled, }: { datafileId: Datafile['id']; onDownloadProgress: (progressEvent: AxiosProgressEvent) => void; -} & UseQueryOptions< - Blob, - AxiosError, - Blob, - ['datafile', 'content', number] ->): UseQueryResult => { + enabled?: boolean; +}) => { const idsUrl = useSelector( (state) => state.dgcommon.urls.idsUrl ); - return useQuery( - ['datafile', 'content', datafileId], - () => + return useQuery({ + // can't track onDownloadProgress in the query key as it's non-serialisable + // so ignore warning + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: ['datafile', 'content', datafileId], + queryFn: () => downloadDatafileToMemory({ idsUrl, datafileId, onDownloadProgress, }), - { - onError: (error) => { - handleICATError(error); - }, - ...queryOptions, - } - ); + meta: { icatError: true }, + enabled, + }); }; /** diff --git a/packages/datagateway-common/src/api/datasets.test.tsx b/packages/datagateway-common/src/api/datasets.test.tsx index 8b1d5a8e8..bd32aab87 100644 --- a/packages/datagateway-common/src/api/datasets.test.tsx +++ b/packages/datagateway-common/src/api/datasets.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios from 'axios'; import { History, createMemoryHistory } from 'history'; import { Dataset } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; import { downloadDataset, @@ -12,12 +12,14 @@ import { useDatasetsPaginated, } from './datasets'; -vi.mock('../handleICATError'); - describe('dataset api functions', () => { let mockData: Dataset[] = []; let history: History; let params: URLSearchParams; + const handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); + beforeEach(() => { mockData = [ { @@ -48,7 +50,7 @@ describe('dataset api functions', () => { }); afterEach(() => { - vi.mocked(handleICATError).mockClear(); + vi.mocked(handleICATErrorSpy).mockClear(); vi.mocked(axios.get).mockClear(); vi.useRealTimers(); }); @@ -100,7 +102,7 @@ describe('dataset api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData); @@ -117,7 +119,7 @@ describe('dataset api functions', () => { expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2); }); - it('sends axios request to fetch paginated datasets and calls handleICATError on failure', async () => { + it('sends axios request to fetch paginated datasets and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -137,10 +139,15 @@ describe('dataset api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -193,14 +200,12 @@ describe('dataset api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([mockData[0]]); + expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - await result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + await result.current.fetchNextPage(); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -213,11 +218,11 @@ describe('dataset api functions', () => { ); params.set('skip', JSON.stringify(50)); params.set('limit', JSON.stringify(25)); - expect(vi.mocked(axios.get).mock.calls[1][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[1][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([ + expect(result.current.data?.pages).toStrictEqual([ mockData[0], mockData[1], ]); @@ -234,7 +239,7 @@ describe('dataset api functions', () => { expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(3); }); - it('sends axios request to fetch infinite datasets and calls handleICATError on failure', async () => { + it('sends axios request to fetch infinite datasets and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -254,10 +259,15 @@ describe('dataset api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -296,13 +306,13 @@ describe('dataset api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData.length); }); - it('sends axios request to fetch dataset count and calls handleICATError on failure', async () => { + it('sends axios request to fetch dataset count and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -318,10 +328,15 @@ describe('dataset api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -352,13 +367,13 @@ describe('dataset api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData[0]); }); - it('sends axios request to fetch dataset details and calls handleICATError on failure', async () => { + it('sends axios request to fetch dataset details and calls handleICATErrorSpy on failure', async () => { const error = axios.AxiosError.from(new Error('Test error')); vi.mocked(axios.get).mockRejectedValue(error); const { result } = renderHook(() => useDatasetDetails(1), { @@ -367,7 +382,7 @@ describe('dataset api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith(error); + expect(handleICATErrorSpy).toHaveBeenCalledWith(error); }); }); diff --git a/packages/datagateway-common/src/api/datasets.tsx b/packages/datagateway-common/src/api/datasets.tsx index d4830acd3..ae658ac4a 100644 --- a/packages/datagateway-common/src/api/datasets.tsx +++ b/packages/datagateway-common/src/api/datasets.tsx @@ -1,5 +1,4 @@ import { - UseInfiniteQueryResult, UseQueryResult, useInfiniteQuery, useQuery, @@ -7,17 +6,17 @@ import { import axios, { AxiosError } from 'axios'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { IndexRange } from 'react-virtualized'; import { getApiParams, parseSearchToQuery, useEntity } from '.'; import { AdditionalFilters, Dataset, FiltersType, + SkipAndLimitType, SortType, } from '../app.types'; -import handleICATError from '../handleICATError'; import { readSciGatewayToken } from '../parseTokens'; import { StateType } from '../state/app.types'; +import { INFINITE_SCROLL_BATCH_SIZE } from '../table/table.component'; import { useRetryICATErrors } from './retryICATErrors'; export const fetchDatasets = ( @@ -27,16 +26,13 @@ export const fetchDatasets = ( filters: FiltersType; }, additionalFilters?: AdditionalFilters, - offsetParams?: IndexRange + skipAndLimit?: SkipAndLimitType ): Promise => { const params = getApiParams(sortAndFilters); - if (offsetParams) { - params.append('skip', JSON.stringify(offsetParams.startIndex)); - params.append( - 'limit', - JSON.stringify(offsetParams.stopIndex - offsetParams.startIndex + 1) - ); + if (skipAndLimit) { + params.append('skip', JSON.stringify(skipAndLimit.skip)); + params.append('limit', JSON.stringify(skipAndLimit.limit)); } additionalFilters?.forEach((filter) => { @@ -58,28 +54,14 @@ export const fetchDatasets = ( export const useDatasetsPaginated = ( additionalFilters?: AdditionalFilters, isMounted?: boolean -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort, page, results } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - Dataset[], - AxiosError, - Dataset[], - [ - string, - { - sort: string; - filters: FiltersType; - page: number; - results: number; - }, - AdditionalFilters? - ] - >( - [ + return useQuery({ + queryKey: [ 'dataset', { sort: JSON.stringify(sort), // need to stringify sort as property order is important! @@ -88,54 +70,55 @@ export const useDatasetsPaginated = ( results: results ?? 10, }, additionalFilters, - ], - (params) => { + apiUrl, + ] as const, + queryFn: (params) => { const { page, results } = params.queryKey[1]; - const startIndex = (page - 1) * results; - const stopIndex = startIndex + results - 1; + const skip = (page - 1) * results; + const limit = results; return fetchDatasets(apiUrl, { sort, filters }, additionalFilters, { - startIndex, - stopIndex, + skip, + limit, }); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const useDatasetsInfinite = ( additionalFilters?: AdditionalFilters, isMounted?: boolean -): UseInfiniteQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery( - ['dataset', { sort: JSON.stringify(sort), filters }, additionalFilters], // need to stringify sort as property order is important! - (params) => { - const offsetParams = params.pageParam ?? { startIndex: 0, stopIndex: 49 }; - return fetchDatasets( + return useInfiniteQuery({ + queryKey: [ + 'dataset', + { sort: JSON.stringify(sort), filters }, + additionalFilters, + apiUrl, + ], // need to stringify sort as property order is important! + queryFn: (params) => + fetchDatasets( apiUrl, { sort, filters }, additionalFilters, - offsetParams - ); - }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + params.pageParam + ), + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const fetchDatasetCountQuery = ( @@ -162,32 +145,27 @@ export const fetchDatasetCountQuery = ( .then((response) => response.data); }; -export const useDatasetCount = ( - additionalFilters?: AdditionalFilters -): UseQueryResult => { +export const useDatasetCount = (additionalFilters?: AdditionalFilters) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const filters = parseSearchToQuery(location.search).filters; const retryICATErrors = useRetryICATErrors(); - return useQuery< - number, - AxiosError, - number, - [string, string, { filters: FiltersType }, AdditionalFilters?] - >( - ['count', 'dataset', { filters }, additionalFilters], - (params) => { + return useQuery({ + queryKey: [ + 'count', + 'dataset', + { filters }, + additionalFilters, + apiUrl, + ] as const, + queryFn: (params) => { const { filters } = params.queryKey[2]; return fetchDatasetCountQuery(apiUrl, filters, additionalFilters); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; export const useDatasetDetails = ( diff --git a/packages/datagateway-common/src/api/dois.test.tsx b/packages/datagateway-common/src/api/dois.test.tsx index a71cb54dd..1c07a39b5 100644 --- a/packages/datagateway-common/src/api/dois.test.tsx +++ b/packages/datagateway-common/src/api/dois.test.tsx @@ -25,9 +25,6 @@ import { NotificationType, } from '../state/actions/actions.types'; -vi.mock('loglevel'); -vi.mock('../handleICATError'); - describe('handleDOIAPIError', () => { const localStorageGetItemMock = vi.spyOn( window.localStorage.__proto__, @@ -39,6 +36,7 @@ describe('handleDOIAPIError', () => { let error: AxiosError<{ detail: { msg: string }[] | string; }>; + let logErrorSpy: ReturnType; beforeEach(() => { events = []; @@ -69,6 +67,7 @@ describe('handleDOIAPIError', () => { message: 'Test error message', toJSON: vi.fn(), }; + logErrorSpy = vi.spyOn(log, 'error').mockReturnValue(); }); afterEach(() => { @@ -83,7 +82,7 @@ describe('handleDOIAPIError', () => { handleDOIAPIError(error); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( error.response?.data?.detail?.[0]?.msg ); expect(events.length).toBe(1); @@ -101,9 +100,9 @@ describe('handleDOIAPIError', () => { return name === 'autoLogin' ? 'false' : null; }); - handleDOIAPIError(error, undefined, undefined, false); + handleDOIAPIError(error, false); - expect(log.error).not.toHaveBeenCalled(); + expect(logErrorSpy).not.toHaveBeenCalled(); expect(events.length).toBe(1); expect(events[0].detail).toEqual({ type: InvalidateTokenType, @@ -120,18 +119,18 @@ describe('handleDOIAPIError', () => { error.response.data.detail = 'Test error message (response data) (string detail)'; } - handleDOIAPIError(error, undefined, undefined, true); + handleDOIAPIError(error, true); - expect(log.error).toHaveBeenCalledWith(error.response.data.detail); + expect(logErrorSpy).toHaveBeenCalledWith(error.response.data.detail); expect(events.length).toBe(0); }); it('should handle other errors by broadcasting a message if broadcast condition is true', async () => { error.response = undefined; - handleDOIAPIError(error, undefined, undefined, false, true); + handleDOIAPIError(error, false, true); - expect(log.error).not.toHaveBeenCalled(); + expect(logErrorSpy).not.toHaveBeenCalled(); expect(events.length).toBe(1); expect(events[0].detail).toEqual({ type: NotificationType, @@ -195,6 +194,12 @@ describe('isMintabilityErrorExpected', () => { }); describe('doi api functions', () => { + let logErrorSpy: ReturnType; + + beforeEach(() => { + logErrorSpy = vi.spyOn(log, 'error').mockReturnValue(); + }); + afterEach(() => { vi.clearAllMocks(); }); @@ -211,7 +216,7 @@ describe('doi api functions', () => { wrapper: createReactQueryWrapper(), } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); act(() => { result.current.refetch(); @@ -239,14 +244,14 @@ describe('doi api functions', () => { wrapper: createReactQueryWrapper(), } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); act(() => { result.current.refetch(); }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.get).toHaveBeenCalledTimes(1); }); @@ -265,14 +270,14 @@ describe('doi api functions', () => { wrapper: createReactQueryWrapper(), } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); act(() => { result.current.refetch(); }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.get).toHaveBeenCalledTimes(1); }); @@ -291,14 +296,14 @@ describe('doi api functions', () => { wrapper: createReactQueryWrapper(), } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); act(() => { result.current.refetch(); }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.get).toHaveBeenCalledTimes(1); }); @@ -317,14 +322,14 @@ describe('doi api functions', () => { wrapper: createReactQueryWrapper(), } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); act(() => { result.current.refetch(); }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.get).toHaveBeenCalledTimes(4); }); }); @@ -409,7 +414,7 @@ describe('doi api functions', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.post).toHaveBeenCalledWith( expect.stringContaining('/draft/pid/version'), @@ -497,7 +502,7 @@ describe('doi api functions', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.put).toHaveBeenCalledWith( expect.stringContaining('/draft/pid/version/new.version.pid/publish'), undefined, @@ -551,7 +556,7 @@ describe('doi api functions', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.delete).toHaveBeenCalledWith( expect.stringContaining('/draft/pid/version/new.version.pid'), { headers: { Authorization: 'Bearer null' } } @@ -621,7 +626,7 @@ describe('doi api functions', () => { { wrapper: createReactQueryWrapper() } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); expect(axios.post).not.toHaveBeenCalled(); }); @@ -672,7 +677,7 @@ describe('doi api functions', () => { ); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).not.toHaveBeenCalled(); + expect(logErrorSpy).not.toHaveBeenCalled(); expect(axios.post).toHaveBeenCalledTimes(1); }); }); @@ -723,7 +728,7 @@ describe('doi api functions', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(log.error).toHaveBeenCalledWith(error.message); + expect(logErrorSpy).toHaveBeenCalledWith(error.message); expect(axios.put).toHaveBeenCalledWith( expect.stringContaining('/open/1'), {}, @@ -764,7 +769,7 @@ describe('useDOI', () => { wrapper: createReactQueryWrapper(), }); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); expect(axios.get).not.toHaveBeenCalled(); }); @@ -860,7 +865,7 @@ describe('BioPortal API functions', () => { } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); expect(axios.get).not.toHaveBeenCalled(); }); @@ -926,7 +931,7 @@ describe('BioPortal API functions', () => { } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); expect(axios.get).not.toHaveBeenCalled(); }); @@ -939,7 +944,7 @@ describe('BioPortal API functions', () => { } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); expect(axios.get).not.toHaveBeenCalled(); }); diff --git a/packages/datagateway-common/src/api/dois.tsx b/packages/datagateway-common/src/api/dois.tsx index 50fbc38e0..6d7a65469 100644 --- a/packages/datagateway-common/src/api/dois.tsx +++ b/packages/datagateway-common/src/api/dois.tsx @@ -1,5 +1,4 @@ import { - UseMutationResult, UseQueryResult, useMutation, useQuery, @@ -32,8 +31,6 @@ export const handleDOIAPIError = ( error: AxiosError<{ detail: { msg: string }[] | string; }>, - _variables?: unknown, - _context?: unknown, logCondition?: boolean, broadcastCondition?: boolean ): void => { @@ -90,7 +87,7 @@ export const handleDOIAPIError = ( export const checkUser = ( username: string, doiMinterUrl: string | undefined -): Promise => { +): Promise => { return axios .get(`${doiMinterUrl}/user/${username}`, { headers: { @@ -110,35 +107,33 @@ export const checkUser = ( export const useCheckUser = ( username: string, doiMinterUrl: string | undefined -): UseQueryResult => { +) => { const queryClient = useQueryClient(); const opts = queryClient.getDefaultOptions(); const retries = typeof opts?.queries?.retry === 'number' ? opts.queries.retry : 3; - return useQuery( - ['checkUser', username], - () => checkUser(username, doiMinterUrl), - { - onError: handleDOIAPIError, - retry: (failureCount: number, error: AxiosError) => { - if ( - // user not logged in, error code will log them out - error.response?.status === 401 || - // email doesn't match user - don't retry as this is a correct response from the server - error.response?.status === 404 || - // email is invalid - don't retry as this is correct response from the server - error.response?.status === 422 || - failureCount >= retries - ) - return false; - return true; - }, - // set enabled false to only fetch on demand when the add creator button is pressed - enabled: false, - cacheTime: 0, - } - ); + return useQuery({ + queryKey: ['checkUser', username, doiMinterUrl], + queryFn: () => checkUser(username, doiMinterUrl), + meta: { icatError: true }, + retry: (failureCount: number, error: AxiosError) => { + if ( + // user not logged in, error code will log them out + error.response?.status === 401 || + // email doesn't match user - don't retry as this is a correct response from the server + error.response?.status === 404 || + // email is invalid - don't retry as this is correct response from the server + error.response?.status === 422 || + failureCount >= retries + ) + return false; + return true; + }, + // set enabled false to only fetch on demand when the add creator button is pressed + enabled: false, + staleTime: 0, + }); }; /** @@ -172,7 +167,9 @@ export const useDOI = ( const retries = typeof opts?.queries?.retry === 'number' ? opts.queries.retry : 3; - return useQuery(['doi', doi], () => fetchDOI(doi ?? '', dataCiteUrl), { + return useQuery({ + queryKey: ['doi', doi, dataCiteUrl], + queryFn: () => fetchDOI(doi ?? '', dataCiteUrl), retry: (failureCount: number, error: AxiosError) => { if ( // DOI is invalid - don't retry as this is a correct response from the server @@ -191,16 +188,15 @@ export const useDOI = ( * @param doi The DOI that we're checking * @returns the {@link RelatedIdentifier} that matches the username, or 404 */ -export const useCheckDOI = ( - doi: string, - dataCiteUrl: string | undefined -): UseQueryResult => { +export const useCheckDOI = (doi: string, dataCiteUrl: string | undefined) => { const queryClient = useQueryClient(); const opts = queryClient.getDefaultOptions(); const retries = typeof opts?.queries?.retry === 'number' ? opts.queries.retry : 3; - return useQuery(['checkDOI', doi], () => fetchDOI(doi, dataCiteUrl), { + return useQuery({ + queryKey: ['checkDOI', doi, dataCiteUrl], + queryFn: () => fetchDOI(doi, dataCiteUrl), retry: (failureCount: number, error: AxiosError) => { if ( // DOI is invalid - don't retry as this is a correct response from the server @@ -210,15 +206,16 @@ export const useCheckDOI = ( return false; return true; }, - select: (doi) => ({ - title: doi.attributes.titles[0].title, - identifier: doi.attributes.doi, - relatedIdentifierType: DOIIdentifierType.DOI, - relationType: '', - }), + select: (doi) => + ({ + title: doi.attributes.titles[0].title, + identifier: doi.attributes.doi, + relatedIdentifierType: DOIIdentifierType.DOI, + relationType: '', + }) satisfies RelatedIdentifier as RelatedIdentifier, // set enabled false to only fetch on demand when the add creator button is pressed enabled: false, - cacheTime: 0, + staleTime: 0, }); }; @@ -263,27 +260,25 @@ export const draftVersionDOI = ( * @param cart The {@link Cart} to mint * @param doiMetadata The required metadata for the DOI */ -export const useDraftVersionDOI = (): UseMutationResult< - DOIDraftVersionResponse, - AxiosError<{ - detail: { msg: string }[] | string; - }>, - { - contentDataPublicationId: string; - content: { - investigation_ids: number[]; - dataset_ids: number[]; - datafile_ids: number[]; - }; - doiMetadata: DOIMetadata; - } -> => { +export const useDraftVersionDOI = () => { const doiMinterUrl = useSelector( (state: StateType) => state.dgcommon.urls.doiMinterUrl ); - return useMutation( - ({ contentDataPublicationId, content, doiMetadata }) => { + return useMutation({ + mutationFn: ({ + contentDataPublicationId, + content, + doiMetadata, + }: { + contentDataPublicationId: string; + content: { + investigation_ids: number[]; + dataset_ids: number[]; + datafile_ids: number[]; + }; + doiMetadata: DOIMetadata; + }) => { return draftVersionDOI( contentDataPublicationId, content, @@ -291,12 +286,15 @@ export const useDraftVersionDOI = (): UseMutationResult< doiMinterUrl ); }, - { - onError: (error) => { - handleDOIAPIError(error, undefined, undefined, true, true); - }, - } - ); + + onError: ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + handleDOIAPIError(error, true, true); + }, + }); }; /** @@ -329,65 +327,66 @@ type UsePublishDraftVersionVariables = { * Publishes a draft data publication * @param dataPublicationId The {@link DataPublication} to publish */ -export const usePublishDraftVersion = (): UseMutationResult< - DOIResponse, - AxiosError<{ - detail: { msg: string }[] | string; - }>, - UsePublishDraftVersionVariables -> => { - const queryClient = useQueryClient(); +export const usePublishDraftVersion = () => { const doiMinterUrl = useSelector( (state: StateType) => state.dgcommon.urls.doiMinterUrl ); const username = readSciGatewayToken().username; - return useMutation( - ({ contentDataPublicationId, draftVersionDataPublicationId }) => { - return publishDraftVersionDOI( + return useMutation({ + mutationFn: ({ + contentDataPublicationId, + draftVersionDataPublicationId, + }: UsePublishDraftVersionVariables) => + publishDraftVersionDOI( contentDataPublicationId, draftVersionDataPublicationId, doiMinterUrl - ); + ), + onError: ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + handleDOIAPIError(error, true, true); }, - { - onError: handleDOIAPIError, - onSuccess: ( - data, - { contentDataPublicationId }: UsePublishDraftVersionVariables - ) => { - // resetQueries instead of invalidateQueries as otherwise invalidateQueries shows out-of-date data - queryClient.resetQueries({ - predicate: (query) => - // invalidate the my DOIs page query - (query.queryKey[0] === 'dataPublication' && - username !== null && - typeof query.queryKey[2] !== 'undefined' && - JSON.stringify(query.queryKey[2]).includes(username) && - JSON.stringify(query.queryKey[2]).includes('User-defined')) || - // invalidate the data publication info query - (query.queryKey[0] === 'dataPublication' && - typeof query.queryKey[1] !== 'undefined' && - JSON.stringify(query.queryKey[1]).includes( - contentDataPublicationId - )) || - // invalidate the data publication datacite info query - (query.queryKey[0] === 'doi' && - typeof query.queryKey[1] !== 'undefined' && - JSON.stringify(query.queryKey[1]).includes( - data.concept.attributes.doi - )) || - // invalidate the data publication content table queries - (query.queryKey[0] === 'dataPublicationContent' && - // use double equals to ignore difference between 1 and "1" - - query.queryKey[2] == contentDataPublicationId) || - (query.queryKey[0] === 'dataPublicationContentCount' && - query.queryKey[2] == contentDataPublicationId), - }); - }, - } - ); + onSuccess: ( + data, + { contentDataPublicationId }: UsePublishDraftVersionVariables, + _onMutateResult, + context + ) => { + // resetQueries instead of invalidateQueries as otherwise invalidateQueries shows out-of-date data + context.client.resetQueries({ + predicate: (query) => + // invalidate the my DOIs page query + (query.queryKey[0] === 'dataPublication' && + username !== null && + typeof query.queryKey[2] !== 'undefined' && + JSON.stringify(query.queryKey[2]).includes(username) && + JSON.stringify(query.queryKey[2]).includes('User-defined')) || + // invalidate the data publication info query + (query.queryKey[0] === 'dataPublication' && + typeof query.queryKey[1] !== 'undefined' && + JSON.stringify(query.queryKey[1]).includes( + contentDataPublicationId + )) || + // invalidate the data publication datacite info query + (query.queryKey[0] === 'doi' && + typeof query.queryKey[1] !== 'undefined' && + JSON.stringify(query.queryKey[1]).includes( + data.concept.attributes.doi + )) || + // invalidate the data publication content table queries + (query.queryKey[0] === 'dataPublicationContent' && + // use double equals to ignore difference between 1 and "1" + + query.queryKey[2] == contentDataPublicationId) || + (query.queryKey[0] === 'dataPublicationContentCount' && + query.queryKey[2] == contentDataPublicationId), + }); + }, + }); }; /** @@ -412,31 +411,31 @@ export const deleteDraftVersionDOI = ( * Deletes a draft version data publication * @param dataPublicationId The {@link DataPublication} to publish */ -export const useDeleteDraftVersion = (): UseMutationResult< - void, - AxiosError<{ - detail: { msg: string }[] | string; - }>, - UsePublishDraftVersionVariables -> => { +export const useDeleteDraftVersion = () => { const doiMinterUrl = useSelector( (state: StateType) => state.dgcommon.urls.doiMinterUrl ); - return useMutation( - ({ contentDataPublicationId, draftVersionDataPublicationId }) => { + return useMutation({ + mutationFn: ({ + contentDataPublicationId, + draftVersionDataPublicationId, + }: UsePublishDraftVersionVariables) => { return deleteDraftVersionDOI( contentDataPublicationId, draftVersionDataPublicationId, doiMinterUrl ); }, - { - onError: (error) => { - handleDOIAPIError(error, undefined, undefined, true, true); - }, - } - ); + + onError: ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + handleDOIAPIError(error, true, true); + }, + }); }; /** @@ -508,36 +507,40 @@ export const useIsCartMintable = ( const retries = typeof opts?.queries?.retry === 'number' ? opts.queries.retry : 3; - return useQuery( - ['ismintable', cart], - () => { + return useQuery({ + queryKey: ['ismintable', cart, doiMinterUrl], + queryFn: () => { if (doiMinterUrl && cart && cart.length > 0) return isCartMintable(cart, doiMinterUrl); else return Promise.resolve(false); }, - { - onError: (error) => { - handleDOIAPIError( - error, - undefined, - undefined, - // don't broadcast or log "expected" errors - !isMintabilityErrorExpected(error), - !isMintabilityErrorExpected(error) - ); - }, - retry: (failureCount, error) => { - // don't bother retrying "expected" errors - all other errors use default retry behaviour - if (isMintabilityErrorExpected(error) || failureCount >= retries) { - return false; - } else { - return true; - } - }, - refetchOnWindowFocus: false, - enabled: typeof doiMinterUrl !== 'undefined', - } - ); + meta: { + DOIAPIError: true, + // don't broadcast or log "expected" errors + broadcastCondition: (error) => + !isMintabilityErrorExpected( + error as AxiosError<{ + detail: { msg: string }[] | string; + }> + ), + logCondition: (error) => + !isMintabilityErrorExpected( + error as AxiosError<{ + detail: { msg: string }[] | string; + }> + ), + }, + retry: (failureCount, error) => { + // don't bother retrying "expected" errors - all other errors use default retry behaviour + if (isMintabilityErrorExpected(error) || failureCount >= retries) { + return false; + } else { + return true; + } + }, + refetchOnWindowFocus: false, + enabled: typeof doiMinterUrl !== 'undefined', + }); }; export const openDataPublication: ( @@ -558,25 +561,22 @@ export const openDataPublication: ( /** * A React hook for opening a session DOI. */ -export const useOpenDataPublication = (): UseMutationResult< - void, - AxiosError<{ - detail: { msg: string }[] | string; - }>, - { dataPublicationId: string } -> => { +export const useOpenDataPublication = () => { const doiMinterUrl = useSelector( (state: StateType) => state.dgcommon.urls.doiMinterUrl ); - return useMutation( - ({ dataPublicationId }) => + return useMutation({ + mutationFn: ({ dataPublicationId }: { dataPublicationId: string }) => openDataPublication(dataPublicationId, doiMinterUrl), - { - onError: (error) => { - handleDOIAPIError(error, undefined, undefined, true, true); - }, - } - ); + + onError: ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + handleDOIAPIError(error, true, true); + }, + }); }; export interface BioPortalResponse { @@ -638,14 +638,12 @@ export const useSearchPANETTechniques = ( searchText: string, bioportalUrl: string | undefined ): UseQueryResult => { - return useQuery( - ['SearchPANETTechniques', searchText], - () => fetchPANETTechniquesFromSearchText(searchText, bioportalUrl), - { - staleTime: Infinity, - enabled: typeof bioportalUrl !== 'undefined', - } - ); + return useQuery({ + queryKey: ['SearchPANETTechniques', searchText, bioportalUrl], + queryFn: () => fetchPANETTechniquesFromSearchText(searchText, bioportalUrl), + staleTime: Infinity, + enabled: typeof bioportalUrl !== 'undefined', + }); }; /** @@ -657,19 +655,16 @@ export const useGetDescendantTechniques = ( selectedTechnique: BioPortalTerm | null, bioportalUrl: string | undefined ): UseQueryResult => { - return useQuery( - ['getDescendantTechniques', selectedTechnique], - () => + return useQuery({ + queryKey: ['getDescendantTechniques', selectedTechnique, bioportalUrl], + queryFn: () => selectedTechnique ? fetchDescendantPANETTechniques( selectedTechnique?.links.descendants, bioportalUrl ) : Promise.resolve([]), - { - staleTime: Infinity, - enabled: - selectedTechnique !== null && typeof bioportalUrl !== 'undefined', - } - ); + staleTime: Infinity, + enabled: selectedTechnique !== null && typeof bioportalUrl !== 'undefined', + }); }; diff --git a/packages/datagateway-common/src/api/facilityCycles.test.tsx b/packages/datagateway-common/src/api/facilityCycles.test.tsx index 0943f9382..fb52f99be 100644 --- a/packages/datagateway-common/src/api/facilityCycles.test.tsx +++ b/packages/datagateway-common/src/api/facilityCycles.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios from 'axios'; import { History, createMemoryHistory } from 'history'; import { FacilityCycle } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; import { useAllFacilityCycles, @@ -11,12 +11,14 @@ import { useFacilityCyclesPaginated, } from './facilityCycles'; -vi.mock('../handleICATError'); - describe('facility cycle api functions', () => { let mockData: FacilityCycle[] = []; let history: History; let params: URLSearchParams; + const handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); + beforeEach(() => { mockData = [ { @@ -43,7 +45,7 @@ describe('facility cycle api functions', () => { }); afterEach(() => { - vi.mocked(handleICATError).mockClear(); + vi.mocked(handleICATErrorSpy).mockClear(); vi.mocked(axios.get).mockClear(); }); @@ -68,7 +70,7 @@ describe('facility cycle api functions', () => { expect(result.current.data).toEqual(mockData); }); - it('sends axios request to fetch all facility cycles and calls handleICATError on failure', async () => { + it('sends axios request to fetch all facility cycles and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -78,7 +80,10 @@ describe('facility cycle api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { message: 'Test error' }, + undefined + ); }); }); @@ -125,7 +130,7 @@ describe('facility cycle api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData); @@ -142,7 +147,7 @@ describe('facility cycle api functions', () => { expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(2); }); - it('sends axios request to fetch paginated facility cycles and calls handleICATError on failure', async () => { + it('sends axios request to fetch paginated facility cycles and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -175,10 +180,13 @@ describe('facility cycle api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { message: 'Test error' }, + undefined + ); }); }); @@ -227,14 +235,12 @@ describe('facility cycle api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([mockData[0]]); + expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - await result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + await result.current.fetchNextPage(); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -247,11 +253,11 @@ describe('facility cycle api functions', () => { ); params.set('skip', JSON.stringify(50)); params.set('limit', JSON.stringify(25)); - expect(vi.mocked(axios.get).mock.calls[1][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[1][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([ + expect(result.current.data?.pages).toStrictEqual([ mockData[0], mockData[1], ]); @@ -268,7 +274,7 @@ describe('facility cycle api functions', () => { expect(vi.mocked(axios.get)).toHaveBeenCalledTimes(3); }); - it('sends axios request to fetch infinite facility cycles and calls handleICATError on failure', async () => { + it('sends axios request to fetch infinite facility cycles and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -301,10 +307,13 @@ describe('facility cycle api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { message: 'Test error' }, + undefined + ); }); }); @@ -347,13 +356,13 @@ describe('facility cycle api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData.length); }); - it('sends axios request to fetch facility cycle count and calls handleICATError on failure', async () => { + it('sends axios request to fetch facility cycle count and calls handleICATErrorSpy on failure', async () => { vi.mocked(axios.get).mockRejectedValue({ message: 'Test error', }); @@ -384,10 +393,13 @@ describe('facility cycle api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { message: 'Test error' }, + undefined + ); }); }); }); diff --git a/packages/datagateway-common/src/api/facilityCycles.tsx b/packages/datagateway-common/src/api/facilityCycles.tsx index 49c1bef47..948d8c3c3 100644 --- a/packages/datagateway-common/src/api/facilityCycles.tsx +++ b/packages/datagateway-common/src/api/facilityCycles.tsx @@ -1,18 +1,17 @@ -import axios, { AxiosError } from 'axios'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { IndexRange } from 'react-virtualized'; import { getApiParams, parseSearchToQuery } from '.'; -import handleICATError from '../handleICATError'; +import { + FacilityCycle, + FiltersType, + SkipAndLimitType, + SortType, +} from '../app.types'; import { readSciGatewayToken } from '../parseTokens'; -import { FiltersType, FacilityCycle, SortType } from '../app.types'; import { StateType } from '../state/app.types'; -import { - useQuery, - UseQueryResult, - useInfiniteQuery, - UseInfiniteQueryResult, -} from '@tanstack/react-query'; +import { INFINITE_SCROLL_BATCH_SIZE } from '../table/table.component'; import { useRetryICATErrors } from './retryICATErrors'; const fetchFacilityCycles = ( @@ -22,16 +21,13 @@ const fetchFacilityCycles = ( sort: SortType; filters: FiltersType; }, - offsetParams?: IndexRange + skipAndLimit?: SkipAndLimitType ): Promise => { const params = getApiParams(sortAndFilters); - if (offsetParams) { - params.append('skip', JSON.stringify(offsetParams.startIndex)); - params.append( - 'limit', - JSON.stringify(offsetParams.stopIndex - offsetParams.startIndex + 1) - ); + if (skipAndLimit) { + params.append('skip', JSON.stringify(skipAndLimit.skip)); + params.append('limit', JSON.stringify(skipAndLimit.limit)); } params.append( @@ -75,50 +71,30 @@ export const fetchAllFacilityCycles = ( }); }; -export const useAllFacilityCycles = ( - enabled?: boolean -): UseQueryResult => { +export const useAllFacilityCycles = (enabled?: boolean) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['facilityCycle'], - () => fetchAllFacilityCycles(apiUrl), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled, - } - ); + return useQuery({ + queryKey: ['facilityCycle', apiUrl], + queryFn: () => fetchAllFacilityCycles(apiUrl), + meta: { icatError: true }, + retry: retryICATErrors, + enabled, + }); }; export const useFacilityCyclesPaginated = ( instrumentId: number, isMounted?: boolean -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort, page, results } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - FacilityCycle[], - AxiosError, - FacilityCycle[], - [ - string, - number, - { - sort: string; - filters: FiltersType; - page: number; - results: number; - } - ] - >( - [ + return useQuery({ + queryKey: [ 'facilityCycle', instrumentId, { @@ -127,59 +103,62 @@ export const useFacilityCyclesPaginated = ( page: page ?? 1, results: results ?? 10, }, - ], - (params) => { + + apiUrl, + ] as const, + queryFn: (params) => { const { page, results } = params.queryKey[2]; - const startIndex = (page - 1) * results; - const stopIndex = startIndex + results - 1; + const skip = (page - 1) * results; + const limit = results; return fetchFacilityCycles( apiUrl, instrumentId, { sort, filters }, { - startIndex, - stopIndex, + skip, + limit, } ); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const useFacilityCyclesInfinite = ( instrumentId: number, isMounted?: boolean -): UseInfiniteQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery( - ['facilityCycle', instrumentId, { sort: JSON.stringify(sort), filters }], - (params) => { - const offsetParams = params.pageParam ?? { startIndex: 0, stopIndex: 49 }; - return fetchFacilityCycles( + return useInfiniteQuery({ + queryKey: [ + 'facilityCycle', + instrumentId, + { sort: JSON.stringify(sort), filters }, + + apiUrl, + ], + queryFn: (params) => + fetchFacilityCycles( apiUrl, instrumentId, { sort, filters }, - offsetParams - ); - }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + params.pageParam + ), + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; const fetchFacilityCycleCount = ( @@ -217,30 +196,25 @@ const fetchFacilityCycleCount = ( }); }; -export const useFacilityCycleCount = ( - instrumentId: number -): UseQueryResult => { +export const useFacilityCycleCount = (instrumentId: number) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - number, - AxiosError, - number, - [string, string, number, { filters: FiltersType }] - >( - ['count', 'facilityCycle', instrumentId, { filters }], - (params) => { + return useQuery({ + queryKey: [ + 'count', + 'facilityCycle', + instrumentId, + { filters }, + apiUrl, + ] as const, + queryFn: (params) => { const { filters } = params.queryKey[3]; return fetchFacilityCycleCount(apiUrl, instrumentId, filters); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; diff --git a/packages/datagateway-common/src/api/generic.tsx b/packages/datagateway-common/src/api/generic.tsx index 377257998..ee8f330cf 100644 --- a/packages/datagateway-common/src/api/generic.tsx +++ b/packages/datagateway-common/src/api/generic.tsx @@ -1,8 +1,4 @@ -import { - UseQueryOptions, - UseQueryResult, - useQuery, -} from '@tanstack/react-query'; +import { UseQueryResult, useQuery } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { useSelector } from 'react-redux'; import { @@ -12,7 +8,7 @@ import { MicroFrontendId, } from '../app.types'; import handleICATError from '../handleICATError'; -import { NotificationType } from '../state/actions/actions.types'; +import { NotificationType } from '../main'; import { StateType } from '../state/app.types'; import { fetchDatafiles } from './datafiles'; import { fetchDatasets } from './datasets'; @@ -28,10 +24,7 @@ export function useEntity( filterType: 'include'; filterValue: string; }, - options?: UseQueryOptions< - Investigation | Dataset | Datafile, - AxiosError | Error - > + enabled?: boolean ): UseQueryResult; export function useEntity( entityName: 'dataset', @@ -41,10 +34,7 @@ export function useEntity( filterType: 'include'; filterValue: string; }, - options?: UseQueryOptions< - Investigation | Dataset | Datafile, - AxiosError | Error - > + enabled?: boolean ): UseQueryResult; export function useEntity( entityName: 'datafile', @@ -54,10 +44,7 @@ export function useEntity( filterType: 'include'; filterValue: string; }, - options?: UseQueryOptions< - Investigation | Dataset | Datafile, - AxiosError | Error - > + enabled?: boolean ): UseQueryResult; export function useEntity( entityName: 'investigation' | 'dataset' | 'datafile', @@ -67,10 +54,7 @@ export function useEntity( filterType: 'include'; filterValue: string; }, - options?: UseQueryOptions< - Investigation | Dataset | Datafile, - AxiosError | Error - > + enabled?: boolean ): UseQueryResult; export function useEntity( entityName: 'investigation' | 'dataset' | 'datafile', @@ -80,17 +64,15 @@ export function useEntity( filterType: 'include'; filterValue: string; }, - options?: UseQueryOptions< - Investigation | Dataset | Datafile, - AxiosError | Error - > + enabled?: boolean ): UseQueryResult { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - return useQuery( - [entityName, entityField, fieldValue, includeFilter], - async (_) => { + return useQuery({ + queryKey: [entityName, entityField, fieldValue, includeFilter], + + queryFn: async (_) => { switch (entityName) { case 'investigation': { const investigations = await fetchInvestigations( @@ -153,9 +135,9 @@ export function useEntity( ); } }, - { - onError: (error) => { - // only handle an ICAT error for axios errors aka not the "not found" errors we list above + meta: { + useEntityErrorHandler: (error) => { + // only handle an ICAT error for axios errors aka not the "not found" errors if (axios.isAxiosError(error)) handleICATError(error); else { document.dispatchEvent( @@ -171,12 +153,12 @@ export function useEntity( ); } }, - retry: (failureCount, error) => { - if (axios.isAxiosError(error)) - return retryICATErrors(failureCount, error); - else return false; - }, - ...options, - } - ); + }, + retry: (failureCount, error) => { + if (axios.isAxiosError(error)) + return retryICATErrors(failureCount, error); + else return false; + }, + enabled, + }); } diff --git a/packages/datagateway-common/src/api/index.test.tsx b/packages/datagateway-common/src/api/index.test.tsx index 981711ec6..3017e2dc4 100644 --- a/packages/datagateway-common/src/api/index.test.tsx +++ b/packages/datagateway-common/src/api/index.test.tsx @@ -10,7 +10,7 @@ import { QueryParams, SortType, } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; import { getApiParams, @@ -36,11 +36,16 @@ import { useUpdateView, } from './index'; -vi.mock('../handleICATError'); - describe('generic api functions', () => { + let handleICATErrorSpy: ReturnType; + + beforeEach(() => { + handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); + }); + afterEach(() => { - vi.mocked(handleICATError).mockClear(); vi.mocked(axios.get).mockClear(); }); @@ -1016,7 +1021,7 @@ describe('generic api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual([1, 2, 3]); @@ -1030,7 +1035,7 @@ describe('generic api functions', () => { } ); - expect(result.current.status).toBe('loading'); + expect(result.current.status).toBe('pending'); expect(result.current.fetchStatus).toBe('idle'); expect(axios.get).not.toHaveBeenCalled(); @@ -1046,7 +1051,12 @@ describe('generic api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -1077,7 +1087,7 @@ describe('generic api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(['1', '2', '3']); @@ -1096,7 +1106,12 @@ describe('generic api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -1134,7 +1149,7 @@ describe('generic api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); @@ -1151,7 +1166,7 @@ describe('generic api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[1][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[1][1]?.params.toString()).toBe( params.toString() ); @@ -1168,7 +1183,7 @@ describe('generic api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[2][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[2][1]?.params.toString()).toBe( params.toString() ); @@ -1195,7 +1210,7 @@ describe('generic api functions', () => { expect(result.current.every((query) => query.isError)).toBe(true) ); - expect(handleICATError).toHaveBeenCalledTimes(3); + expect(handleICATErrorSpy).toHaveBeenCalledTimes(3); expect(result.current.map((query) => query.error)).toEqual( Array(3).fill({ message: 'Test error' }) ); diff --git a/packages/datagateway-common/src/api/index.tsx b/packages/datagateway-common/src/api/index.tsx index f7462eb98..9bc7ccf5b 100644 --- a/packages/datagateway-common/src/api/index.tsx +++ b/packages/datagateway-common/src/api/index.tsx @@ -1,10 +1,10 @@ import { - UseQueryOptions, UseQueryResult, + queryOptions, useQueries, useQuery, } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; import { isValid } from 'date-fns'; import format from 'date-fns/format'; import React from 'react'; @@ -22,7 +22,6 @@ import { UpdateMethod, ViewsType, } from '../app.types'; -import handleICATError from '../handleICATError'; import { readSciGatewayToken } from '../parseTokens'; import { StateType } from '../state/app.types'; import { useRetryICATErrors } from './retryICATErrors'; @@ -725,31 +724,27 @@ export const useIds = ( entityType: 'investigation' | 'dataset' | 'datafile', additionalFilters?: AdditionalFilters, enabled = true -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - number[], - AxiosError, - number[], - [string, { filters: FiltersType }, AdditionalFilters?] - >( - [`${entityType}Ids`, { filters }, additionalFilters], - (params) => { + return useQuery({ + queryKey: [ + `${entityType}Ids`, + { filters }, + additionalFilters, + apiUrl, + ] as const, + queryFn: (params) => { const { filters } = params.queryKey[1]; return fetchIds(apiUrl, entityType, filters, additionalFilters); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled, + }); }; const fetchFilter = ( @@ -808,33 +803,17 @@ export const useCustomFilter = ( filterType: 'where' | 'distinct' | 'include'; filterValue: string; }[] -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - return useQuery< - string[], - AxiosError, - string[], - [ - 'investigation' | 'dataset' | 'datafile', - string, - { - filterType: 'where' | 'distinct' | 'include'; - filterValue: string; - }[]?, - ] - >( - [entityType, filterKey, additionalFilters], - ({ queryKey }) => + return useQuery({ + queryKey: [entityType, filterKey, additionalFilters, apiUrl] as const, + queryFn: ({ queryKey }) => fetchFilter(apiUrl, queryKey[0], queryKey[1], queryKey[2]), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; export const formatFilterCount = ( @@ -893,40 +872,22 @@ export const useCustomFilterCount = ( filterType: 'where' | 'distinct' | 'include'; filterValue: string; }[] -): UseQueryResult[] => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - const queryConfigs: UseQueryOptions< - number, - AxiosError, - number, - [ - string, - ( - | 'investigation' - | 'dataset' - | 'datafile' - | 'facilityCycle' - | 'instrument' - | 'facility' - | 'dataPublication' - ), - string, - string, - AdditionalFilters?, - ] - >[] = React.useMemo(() => { + const queryConfigs = React.useMemo(() => { const ids = filterIds ?? []; return ids.map((filterId) => { - return { + return queryOptions({ queryKey: [ 'filterCount', entityType, filterKey, filterId, additionalFilters, + apiUrl, ], queryFn: () => fetchFilterCountQuery(apiUrl, entityType, [ @@ -938,12 +899,10 @@ export const useCustomFilterCount = ( }, ...(additionalFilters ?? []), ]), - onError: (error) => { - handleICATError(error, false); - }, + meta: { icatError: true }, retry: retryICATErrors, staleTime: Infinity, - }; + }); }); }, [ filterIds, @@ -954,10 +913,6 @@ export const useCustomFilterCount = ( apiUrl, ]); - // useQueries doesn't allow us to specify type info, so ignore this line - // since we strongly type the queries object anyway - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore return useQueries({ queries: queryConfigs, }); diff --git a/packages/datagateway-common/src/api/instruments.test.tsx b/packages/datagateway-common/src/api/instruments.test.tsx index 545ceb810..49becfaaf 100644 --- a/packages/datagateway-common/src/api/instruments.test.tsx +++ b/packages/datagateway-common/src/api/instruments.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios from 'axios'; import { History, createMemoryHistory } from 'history'; import { Instrument } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; import { useInstrumentCount, @@ -11,12 +11,14 @@ import { useInstrumentsPaginated, } from './instruments'; -vi.mock('../handleICATError'); - describe('instrument api functions', () => { let mockData: Instrument[] = []; let history: History; let params: URLSearchParams; + const handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); + beforeEach(() => { mockData = [ { @@ -37,7 +39,7 @@ describe('instrument api functions', () => { }); afterEach(() => { - vi.mocked(handleICATError).mockClear(); + vi.mocked(handleICATErrorSpy).mockClear(); vi.mocked(axios.get).mockClear(); }); @@ -71,7 +73,7 @@ describe('instrument api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData); @@ -118,10 +120,15 @@ describe('instrument api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -157,14 +164,12 @@ describe('instrument api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([mockData[0]]); + expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - await result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + await result.current.fetchNextPage(); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -177,11 +182,11 @@ describe('instrument api functions', () => { ); params.set('skip', JSON.stringify(50)); params.set('limit', JSON.stringify(25)); - expect(vi.mocked(axios.get).mock.calls[1][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[1][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([ + expect(result.current.data?.pages).toStrictEqual([ mockData[0], mockData[1], ]); @@ -228,10 +233,15 @@ describe('instrument api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -260,7 +270,7 @@ describe('instrument api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData.length); @@ -282,10 +292,15 @@ describe('instrument api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -318,7 +333,7 @@ describe('instrument api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData[0]); @@ -334,7 +349,12 @@ describe('instrument api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); }); diff --git a/packages/datagateway-common/src/api/instruments.tsx b/packages/datagateway-common/src/api/instruments.tsx index b06b3f59a..c8e1695ed 100644 --- a/packages/datagateway-common/src/api/instruments.tsx +++ b/packages/datagateway-common/src/api/instruments.tsx @@ -1,23 +1,18 @@ -import axios, { AxiosError } from 'axios'; +import { useInfiniteQuery, useQuery } from '@tanstack/react-query'; +import axios from 'axios'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import { IndexRange } from 'react-virtualized'; import { getApiParams, parseSearchToQuery } from '.'; -import handleICATError from '../handleICATError'; -import { readSciGatewayToken } from '../parseTokens'; import { AdditionalFilters, FiltersType, Instrument, + SkipAndLimitType, SortType, } from '../app.types'; +import { readSciGatewayToken } from '../parseTokens'; import { StateType } from '../state/app.types'; -import { - useQuery, - UseQueryResult, - useInfiniteQuery, - UseInfiniteQueryResult, -} from '@tanstack/react-query'; +import { INFINITE_SCROLL_BATCH_SIZE } from '../table/table.component'; import { useRetryICATErrors } from './retryICATErrors'; const fetchInstruments = ( @@ -27,16 +22,13 @@ const fetchInstruments = ( filters: FiltersType; }, additionalFilters?: AdditionalFilters, - offsetParams?: IndexRange + skipAndLimit?: SkipAndLimitType ): Promise => { const params = getApiParams(sortAndFilters); - if (offsetParams) { - params.append('skip', JSON.stringify(offsetParams.startIndex)); - params.append( - 'limit', - JSON.stringify(offsetParams.stopIndex - offsetParams.startIndex + 1) - ); + if (skipAndLimit) { + params.append('skip', JSON.stringify(skipAndLimit.skip)); + params.append('limit', JSON.stringify(skipAndLimit.limit)); } additionalFilters?.forEach((filter) => { @@ -58,27 +50,14 @@ const fetchInstruments = ( export const useInstrumentsPaginated = ( additionalFilters?: AdditionalFilters, isMounted?: boolean -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort, page, results } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - Instrument[], - AxiosError, - Instrument[], - [ - string, - { - sort: string; - filters: FiltersType; - page: number; - results: number; - } - ] - >( - [ + return useQuery({ + queryKey: [ 'instrument', { sort: JSON.stringify(sort), // need to stringify sort as property order is important! @@ -86,54 +65,56 @@ export const useInstrumentsPaginated = ( page: page ?? 1, results: results ?? 10, }, - ], - (params) => { + additionalFilters, + apiUrl, + ] as const, + queryFn: (params) => { const { page, results } = params.queryKey[1]; - const startIndex = (page - 1) * results; - const stopIndex = startIndex + results - 1; + const skip = (page - 1) * results; + const limit = results; return fetchInstruments(apiUrl, { sort, filters }, additionalFilters, { - startIndex, - stopIndex, + skip, + limit, }); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const useInstrumentsInfinite = ( additionalFilters?: AdditionalFilters, isMounted?: boolean -): UseInfiniteQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery( - ['instrument', { sort: JSON.stringify(sort), filters }], // need to stringify sort as property order is important! - (params) => { - const offsetParams = params.pageParam ?? { startIndex: 0, stopIndex: 49 }; - return fetchInstruments( + return useInfiniteQuery({ + queryKey: [ + 'instrument', + { sort: JSON.stringify(sort), filters }, + additionalFilters, + apiUrl, + ], // need to stringify sort as property order is important! + queryFn: (params) => + fetchInstruments( apiUrl, { sort, filters }, additionalFilters, - offsetParams - ); - }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + params.pageParam + ), + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; const fetchInstrumentCount = ( @@ -153,30 +134,21 @@ const fetchInstrumentCount = ( .then((response) => response.data); }; -export const useInstrumentCount = (): UseQueryResult => { +export const useInstrumentCount = () => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - number, - AxiosError, - number, - [string, string, { filters: FiltersType }] - >( - ['count', 'instrument', { filters }], - (params) => { + return useQuery({ + queryKey: ['count', 'instrument', { filters }, apiUrl] as const, + queryFn: (params) => { const { filters } = params.queryKey[2]; return fetchInstrumentCount(apiUrl, filters); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; const fetchInstrumentDetails = ( @@ -197,20 +169,14 @@ const fetchInstrumentDetails = ( .then((response) => response.data[0]); }; -export const useInstrumentDetails = ( - instrumentId: number -): UseQueryResult => { +export const useInstrumentDetails = (instrumentId: number) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['instrumentDetails', instrumentId], - (params) => fetchInstrumentDetails(apiUrl, params.queryKey[1]), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + return useQuery({ + queryKey: ['instrumentDetails', instrumentId, apiUrl] as const, + queryFn: (params) => fetchInstrumentDetails(apiUrl, params.queryKey[1]), + meta: { icatError: true }, + retry: retryICATErrors, + }); }; diff --git a/packages/datagateway-common/src/api/investigations.test.tsx b/packages/datagateway-common/src/api/investigations.test.tsx index 44a3d2049..9fc57ee7c 100644 --- a/packages/datagateway-common/src/api/investigations.test.tsx +++ b/packages/datagateway-common/src/api/investigations.test.tsx @@ -2,7 +2,7 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios from 'axios'; import { History, createMemoryHistory } from 'history'; import { Investigation } from '../app.types'; -import handleICATError from '../handleICATError'; +import * as handleICATError from '../handleICATError'; import { createReactQueryWrapper } from '../setupTests'; import { downloadInvestigation, @@ -12,12 +12,14 @@ import { useInvestigationsPaginated, } from './investigations'; -vi.mock('../handleICATError'); - describe('investigation api functions', () => { let mockData: Investigation[] = []; let history: History; let params: URLSearchParams; + const handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); + beforeEach(() => { mockData = [ { @@ -54,7 +56,7 @@ describe('investigation api functions', () => { }); afterEach(() => { - vi.mocked(handleICATError).mockClear(); + vi.mocked(handleICATErrorSpy).mockClear(); vi.mocked(axios.get).mockClear(); vi.useRealTimers(); }); @@ -106,7 +108,7 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData); @@ -171,7 +173,7 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData); @@ -197,10 +199,15 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -253,14 +260,12 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([mockData[0]]); + expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - await result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + await result.current.fetchNextPage(); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -273,11 +278,11 @@ describe('investigation api functions', () => { ); params.set('skip', JSON.stringify(50)); params.set('limit', JSON.stringify(25)); - expect(vi.mocked(axios.get).mock.calls[1][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[1][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([ + expect(result.current.data?.pages).toStrictEqual([ mockData[0], mockData[1], ]); @@ -344,14 +349,12 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([mockData[0]]); + expect(result.current.data?.pages).toStrictEqual([mockData[0]]); - await result.current.fetchNextPage({ - pageParam: { startIndex: 50, stopIndex: 74 }, - }); + await result.current.fetchNextPage(); await waitFor(() => expect(result.current.isFetching).toBe(false)); @@ -364,11 +367,11 @@ describe('investigation api functions', () => { ); params.set('skip', JSON.stringify(50)); params.set('limit', JSON.stringify(25)); - expect(vi.mocked(axios.get).mock.calls[1][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[1][1]?.params.toString()).toBe( params.toString() ); - expect(result.current.data.pages).toStrictEqual([ + expect(result.current.data?.pages).toStrictEqual([ mockData[0], mockData[1], ]); @@ -394,10 +397,15 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -436,7 +444,7 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData.length); @@ -458,10 +466,15 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); - expect(handleICATError).toHaveBeenCalledWith({ message: 'Test error' }); + expect(handleICATErrorSpy).toHaveBeenCalledWith( + { + message: 'Test error', + }, + undefined + ); }); }); @@ -500,7 +513,7 @@ describe('investigation api functions', () => { params, }) ); - expect(vi.mocked(axios.get).mock.calls[0][1].params.toString()).toBe( + expect(vi.mocked(axios.get).mock.calls[0][1]?.params.toString()).toBe( params.toString() ); expect(result.current.data).toEqual(mockData[0]); @@ -515,7 +528,7 @@ describe('investigation api functions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith(error); + expect(handleICATErrorSpy).toHaveBeenCalledWith(error); }); }); diff --git a/packages/datagateway-common/src/api/investigations.tsx b/packages/datagateway-common/src/api/investigations.tsx index 370652013..a7481680c 100644 --- a/packages/datagateway-common/src/api/investigations.tsx +++ b/packages/datagateway-common/src/api/investigations.tsx @@ -1,5 +1,4 @@ import { - UseInfiniteQueryResult, UseQueryResult, useInfiniteQuery, useQuery, @@ -7,17 +6,17 @@ import { import axios, { AxiosError } from 'axios'; import { useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; -import type { IndexRange } from 'react-virtualized'; import { getApiParams, parseSearchToQuery } from '.'; import type { AdditionalFilters, FiltersType, Investigation, + SkipAndLimitType, SortType, } from '../app.types'; -import handleICATError from '../handleICATError'; import { readSciGatewayToken } from '../parseTokens'; import { StateType } from '../state/app.types'; +import { INFINITE_SCROLL_BATCH_SIZE } from '../table/table.component'; import { useEntity } from './generic'; import { useRetryICATErrors } from './retryICATErrors'; @@ -28,17 +27,14 @@ export const fetchInvestigations = ( filters: FiltersType; }, additionalFilters?: AdditionalFilters, - offsetParams?: IndexRange, + skipAndLimit?: SkipAndLimitType, ignoreIDSort?: boolean ): Promise => { const params = getApiParams(sortAndFilters, ignoreIDSort); - if (offsetParams) { - params.append('skip', JSON.stringify(offsetParams.startIndex)); - params.append( - 'limit', - JSON.stringify(offsetParams.stopIndex - offsetParams.startIndex + 1) - ); + if (skipAndLimit) { + params.append('skip', JSON.stringify(skipAndLimit.skip)); + params.append('limit', JSON.stringify(skipAndLimit.limit)); } additionalFilters?.forEach((filter) => { @@ -61,29 +57,14 @@ export const useInvestigationsPaginated = ( additionalFilters?: AdditionalFilters, ignoreIDSort?: boolean, isMounted?: boolean -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort, page, results } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useQuery< - Investigation[], - AxiosError, - Investigation[], - [ - string, - { - sort: string; - filters: FiltersType; - page: number; - results: number; - }, - AdditionalFilters?, - boolean? - ] - >( - [ + return useQuery({ + queryKey: [ 'investigation', { sort: JSON.stringify(sort), // need to stringify sort as property order is important! @@ -93,67 +74,65 @@ export const useInvestigationsPaginated = ( }, additionalFilters, ignoreIDSort, - ], - (params) => { + apiUrl, + ] as const, + + queryFn: (params) => { const { page, results } = params.queryKey[1]; - const startIndex = (page - 1) * results; - const stopIndex = startIndex + results - 1; + const skip = (page - 1) * results; + const limit = results; return fetchInvestigations( apiUrl, { sort, filters }, additionalFilters, { - startIndex, - stopIndex, + skip, + limit, }, ignoreIDSort ); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const useInvestigationsInfinite = ( additionalFilters?: AdditionalFilters, ignoreIDSort?: boolean, isMounted?: boolean -): UseInfiniteQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const { filters, sort } = parseSearchToQuery(location.search); const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery( - [ + return useInfiniteQuery({ + queryKey: [ 'investigation', { sort: JSON.stringify(sort), filters }, // need to stringify sort as property order is important! additionalFilters, ignoreIDSort, + apiUrl, ], - (params) => { - const offsetParams = params.pageParam ?? { startIndex: 0, stopIndex: 49 }; - return fetchInvestigations( + queryFn: (params) => + fetchInvestigations( apiUrl, { sort, filters }, additionalFilters, - offsetParams, + params.pageParam, ignoreIDSort - ); - }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - enabled: isMounted ?? true, - } - ); + ), + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + retry: retryICATErrors, + enabled: isMounted ?? true, + }); }; export const fetchInvestigationCount = ( @@ -182,30 +161,27 @@ export const fetchInvestigationCount = ( export const useInvestigationCount = ( additionalFilters?: AdditionalFilters -): UseQueryResult => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const location = useLocation(); const filters = parseSearchToQuery(location.search).filters; const retryICATErrors = useRetryICATErrors(); - return useQuery< - number, - AxiosError, - number, - [string, string, { filters: FiltersType }, AdditionalFilters?] - >( - ['count', 'investigation', { filters }, additionalFilters], - (params) => { + return useQuery({ + queryKey: [ + 'count', + 'investigation', + { filters }, + additionalFilters, + apiUrl, + ] as const, + queryFn: (params) => { const { filters } = params.queryKey[2]; return fetchInvestigationCount(apiUrl, filters, additionalFilters); }, - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + }); }; export const useInvestigationDetails = ( diff --git a/packages/datagateway-common/src/api/lucene.test.tsx b/packages/datagateway-common/src/api/lucene.test.tsx index 86373ae59..12dbf90b4 100644 --- a/packages/datagateway-common/src/api/lucene.test.tsx +++ b/packages/datagateway-common/src/api/lucene.test.tsx @@ -1,24 +1,26 @@ import { act, renderHook, waitFor } from '@testing-library/react'; import axios, { type AxiosError } from 'axios'; +import type { DeepPartial } from 'redux'; import { LUCENE_ERROR_CODE, + useLuceneFacet, + useLuceneSearchInfinite, type LuceneError, type LuceneSearchParams, - useLuceneSearchInfinite, - useLuceneFacet, } from '.'; -import handleICATError from '../handleICATError'; -import { createReactQueryWrapper } from '../setupTests'; import type { FiltersType } from '../app.types'; +import * as handleICATError from '../handleICATError'; +import { createReactQueryWrapper } from '../setupTests'; import { NotificationType } from '../state/actions/actions.types'; -import type { DeepPartial } from 'redux'; - -vi.mock('../handleICATError'); describe('Lucene actions', () => { + const handleICATErrorSpy = vi + .spyOn(handleICATError, 'default') + .mockImplementation(vi.fn()); + afterEach(() => { vi.mocked(axios.get).mockClear(); - vi.mocked(handleICATError).mockClear(); + vi.mocked(handleICATErrorSpy).mockClear(); }); describe('useLuceneSearchInfinite', () => { @@ -382,7 +384,7 @@ describe('Lucene actions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith(axiosError); + expect(handleICATErrorSpy).toHaveBeenCalledWith(axiosError); }); it('for other types of errors', async () => { @@ -414,7 +416,7 @@ describe('Lucene actions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith(axiosError); + expect(handleICATErrorSpy).toHaveBeenCalledWith(axiosError); }); it('for other internal errors', async () => { @@ -446,7 +448,7 @@ describe('Lucene actions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith(axiosError); + expect(handleICATErrorSpy).toHaveBeenCalledWith(axiosError); }); }); }); @@ -464,9 +466,12 @@ describe('Lucene actions', () => { const { result } = renderHook( () => - useLuceneFacet('Investigation', facets, filters, { - select: (data) => data.results, - }), + useLuceneFacet( + 'Investigation', + facets, + filters, + (data) => data.results + ), { wrapper: createReactQueryWrapper() } ); @@ -501,7 +506,7 @@ describe('Lucene actions', () => { await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith('error'); + expect(handleICATErrorSpy).toHaveBeenCalledWith('error', undefined); }); }); }); diff --git a/packages/datagateway-common/src/api/lucene.tsx b/packages/datagateway-common/src/api/lucene.tsx index 63964dcfd..0084ed780 100644 --- a/packages/datagateway-common/src/api/lucene.tsx +++ b/packages/datagateway-common/src/api/lucene.tsx @@ -1,10 +1,7 @@ import { - UseQueryOptions, + InfiniteData, useInfiniteQuery, useQuery, - type UseInfiniteQueryOptions, - type UseInfiniteQueryResult, - type UseQueryResult, } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { format, set } from 'date-fns'; @@ -125,7 +122,7 @@ export interface LuceneError { /** * Provides special handling for some Lucene errors. For all other ICAT/Lucene errors, the error is forwarded to handleICATError. */ -const handleLuceneError = (error: AxiosError): void => { +export const handleLuceneError = (error: AxiosError): void => { const errorResponse = error.response; if (!errorResponse) { handleICATError(error); @@ -291,25 +288,22 @@ export const useLuceneFacet = ( datasearchType: DatasearchType, facetRequests: FacetRequest[], facetFilters: FiltersType, - options: UseQueryOptions< - SearchResponse, - AxiosError, - TSelectData, - [string, DatasearchType, FacetRequest[], FiltersType] - > = {} -): UseQueryResult => { + selectFn?: (data: SearchResponse) => TSelectData +) => { const icatUrl = useSelector( (state: StateType) => state.dgcommon.urls.icatUrl ); - return useQuery< - SearchResponse, - AxiosError, - TSelectData, - [string, DatasearchType, FacetRequest[], FiltersType] - >( - ['facet', datasearchType, facetRequests, facetFilters], - (queryFunctionContext) => { + return useQuery({ + queryKey: [ + 'facet', + datasearchType, + facetRequests, + facetFilters, + icatUrl, + ] as const, + + queryFn: (queryFunctionContext) => { return fetchLuceneFacets( queryFunctionContext.queryKey[1], queryFunctionContext.queryKey[2], @@ -317,27 +311,27 @@ export const useLuceneFacet = ( { icatUrl } ); }, - { - onError: (error) => { - handleICATError(error); - }, - ...options, - } - ); + meta: { icatError: true }, + select: selectFn, + }); }; -export const useLuceneSearchInfinite = ( +export const useLuceneSearchInfinite = < + TSelectData = InfiniteData, +>( datasearchType: DatasearchType, luceneParams: LuceneSearchParams, facetFilters: FiltersType, - options?: UseInfiniteQueryOptions< - SearchResponse, - AxiosError, - SearchResponse, - SearchResponse, - [string, DatasearchType, LuceneSearchParams] - > -): UseInfiniteQueryResult> => { + { + enabled, + select, + }: { + enabled?: boolean; + select?: ( + data: InfiniteData + ) => TSelectData; + } = {} +) => { const icatUrl = useSelector( (state: StateType) => state.dgcommon.urls.icatUrl ); @@ -356,22 +350,20 @@ export const useLuceneSearchInfinite = ( } const retryICATErrors = useRetryICATErrors(); - return useInfiniteQuery( - ['search', datasearchType, apiLuceneParams], - ({ pageParam }) => + return useInfiniteQuery({ + queryKey: ['search', datasearchType, apiLuceneParams, icatUrl], + queryFn: ({ pageParam }) => fetchLuceneData( datasearchType, { ...apiLuceneParams, search_after: pageParam }, { icatUrl } ), - { - onError: (error: AxiosError) => { - handleLuceneError(error); - }, - retry: retryICATErrors, - getNextPageParam: (lastPage, _) => lastPage.search_after, - refetchOnWindowFocus: false, - ...(options ?? {}), - } - ); + getNextPageParam: (lastPage, _) => lastPage.search_after, + initialPageParam: undefined as SearchAfter | undefined, + meta: { luceneError: true }, + retry: retryICATErrors, + refetchOnWindowFocus: false, + enabled, + select, + }); }; diff --git a/packages/datagateway-common/src/api/retryICATErrors.test.ts b/packages/datagateway-common/src/api/retryICATErrors.test.ts index e96076926..f430de580 100644 --- a/packages/datagateway-common/src/api/retryICATErrors.test.ts +++ b/packages/datagateway-common/src/api/retryICATErrors.test.ts @@ -1,8 +1,8 @@ -import { AxiosError } from 'axios'; -import { useRetryICATErrors } from './retryICATErrors'; +import { QueryClient } from '@tanstack/react-query'; import { renderHook } from '@testing-library/react'; +import { AxiosError } from 'axios'; import { createReactQueryWrapper } from '../setupTests'; -import { QueryClient } from '@tanstack/react-query'; +import { useRetryICATErrors } from './retryICATErrors'; // have to unmock here as we mock "globally" in setupTests.tsx vi.unmock('./retryICATErrors'); diff --git a/packages/datagateway-common/src/app.types.tsx b/packages/datagateway-common/src/app.types.tsx index 7d565d0b7..6910fe2f5 100644 --- a/packages/datagateway-common/src/app.types.tsx +++ b/packages/datagateway-common/src/app.types.tsx @@ -14,7 +14,7 @@ export const FACILITY_NAME = { lils: 'LILS', } as const; -export type FacilityName = typeof FACILITY_NAME[keyof typeof FACILITY_NAME]; +export type FacilityName = (typeof FACILITY_NAME)[keyof typeof FACILITY_NAME]; export interface CommonSettings { facilityName: string; @@ -485,6 +485,11 @@ export interface SortType { [column: string]: Order; } +export interface SkipAndLimitType { + skip: number; + limit: number; +} + export type ViewsType = 'table' | 'card' | null; export type DOIViewType = diff --git a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx index 19ac2c6bd..26f9f7987 100644 --- a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx +++ b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.test.tsx @@ -25,7 +25,7 @@ describe('DOI Confirm Dialog component', () => { user = userEvent.setup(); props = { open: true, - mintingStatus: 'loading', + mintingStatus: 'pending', data: undefined, error: null, setClose: vi.fn(), @@ -36,7 +36,7 @@ describe('DOI Confirm Dialog component', () => { vi.clearAllMocks(); }); - it('should show loading indicator when mintingStatus is loading', async () => { + it('should show loading indicator when mintingStatus is pending', async () => { renderComponent(); expect(screen.getByRole('progressbar')).toBeInTheDocument(); diff --git a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx index 597d3bec4..137cc6f66 100644 --- a/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx +++ b/packages/datagateway-common/src/dois/DOIConfirmDialog.component.tsx @@ -36,7 +36,7 @@ const DOIConfirmDialog: React.FC = ( const isMintSuccess = mintingStatus === 'success'; - const isMintLoading = mintingStatus === 'loading'; + const isMintLoading = mintingStatus === 'pending'; const [t] = useTranslation(); diff --git a/packages/datagateway-common/src/dois/creatorsAndContributors.component.test.tsx b/packages/datagateway-common/src/dois/creatorsAndContributors.component.test.tsx index 1026a6985..7acc14bab 100644 --- a/packages/datagateway-common/src/dois/creatorsAndContributors.component.test.tsx +++ b/packages/datagateway-common/src/dois/creatorsAndContributors.component.test.tsx @@ -16,12 +16,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); describe('DOI generation form component', () => { diff --git a/packages/datagateway-common/src/dois/relatedIdentifiers.component.test.tsx b/packages/datagateway-common/src/dois/relatedIdentifiers.component.test.tsx index 0fa6cd000..5ff1ef8e2 100644 --- a/packages/datagateway-common/src/dois/relatedIdentifiers.component.test.tsx +++ b/packages/datagateway-common/src/dois/relatedIdentifiers.component.test.tsx @@ -15,12 +15,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); describe('Related identifiers form component', () => { diff --git a/packages/datagateway-common/src/dois/techniquesAndSubjects.component.test.tsx b/packages/datagateway-common/src/dois/techniquesAndSubjects.component.test.tsx index 0d78ae649..231c3fc77 100644 --- a/packages/datagateway-common/src/dois/techniquesAndSubjects.component.test.tsx +++ b/packages/datagateway-common/src/dois/techniquesAndSubjects.component.test.tsx @@ -25,12 +25,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); describe('Techniques & Subjects selector component', () => { diff --git a/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.test.tsx b/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.test.tsx index 480a880af..9a7593381 100644 --- a/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.test.tsx +++ b/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.test.tsx @@ -6,15 +6,6 @@ import { getDownload, useQueueVisit, useSubmitCart } from '../api'; import DownloadConfirmDialog from './downloadConfirmDialog.component'; vi.mock('../downloadApi'); -vi.mock('datagateway-common', async () => { - const originalModule = await vi.importActual('datagateway-common'); - - return { - __esModule: true, - ...originalModule, - handleICATError: vi.fn(), - }; -}); const createTestQueryClient = (): QueryClient => new QueryClient({ @@ -23,12 +14,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); describe('DownloadConfirmDialog', () => { diff --git a/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.tsx b/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.tsx index 6b22c7dc5..2b003f81a 100644 --- a/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.tsx +++ b/packages/datagateway-common/src/downloadConfirmation/downloadConfirmDialog.component.tsx @@ -114,18 +114,17 @@ const DownloadConfirmDialog: React.FC = ( const { data: submitDownloadData, mutate: submitDownload, - isLoading: isSubmittingDownload, + isPending: isSubmittingDownload, isSuccess: isDownloadSubmittedSuccessfully, isError: hasSubmitDownloadFailed, reset: resetSubmitDownloadMutation, - } = submitDownloadHook(facilityName, downloadApiUrl, undefined); + } = submitDownloadHook(facilityName, downloadApiUrl); // query download after cart is submitted const { data: downloadInfo, isSuccess: isDownloadInfoAvailable, isError: isDownloadInfoUnavailable, - remove: resetDownloadQuery, } = useDownload({ id: typeof submitDownloadData === 'number' ? submitDownloadData : -1, facilityName, @@ -177,12 +176,11 @@ const DownloadConfirmDialog: React.FC = ( React.useEffect(() => { if (props.open) { - resetDownloadQuery(); resetSubmitDownloadMutation(); setDownloadName(''); setEmailAddress(''); } - }, [props.open, resetDownloadQuery, resetSubmitDownloadMutation]); + }, [props.open, resetSubmitDownloadMutation]); React.useEffect(() => { if (props.open) { diff --git a/packages/datagateway-common/src/handleICATError.test.ts b/packages/datagateway-common/src/handleICATError.test.ts index f048a644a..3a669fe91 100644 --- a/packages/datagateway-common/src/handleICATError.test.ts +++ b/packages/datagateway-common/src/handleICATError.test.ts @@ -1,17 +1,16 @@ import { AxiosError } from 'axios'; import log from 'loglevel'; -import handleICATError from './handleICATError'; import { AnyAction } from 'redux'; +import handleICATError from './handleICATError'; import { - NotificationType, InvalidateTokenType, + NotificationType, } from './state/actions/actions.types'; -vi.mock('loglevel'); - describe('handleICATError', () => { let error: AxiosError; let events: CustomEvent[] = []; + const logErrorSpy = vi.spyOn(log, 'error').mockReturnValue(); beforeEach(() => { events = []; @@ -40,7 +39,7 @@ describe('handleICATError', () => { it('logs an error and sends a notification to SciGateway', () => { handleICATError(error); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( 'Test error message (response data)' ); expect(events.length).toBe(1); @@ -71,7 +70,7 @@ describe('handleICATError', () => { handleICATError(error); - expect(log.error).toHaveBeenCalledWith('Test error message'); + expect(logErrorSpy).toHaveBeenCalledWith('Test error message'); expect(events.length).toBe(1); expect(events[0].detail).toEqual({ type: NotificationType, @@ -93,7 +92,7 @@ describe('handleICATError', () => { handleICATError(error); - expect(log.error).toHaveBeenCalledWith('Network Error'); + expect(logErrorSpy).toHaveBeenCalledWith('Network Error'); expect(events.length).toBe(1); expect(events[0].detail).toEqual({ type: NotificationType, @@ -107,7 +106,7 @@ describe('handleICATError', () => { it('just logs an error if broadcast is false', () => { handleICATError(error, false); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( 'Test error message (response data)' ); expect(events.length).toBe(0); @@ -127,7 +126,7 @@ describe('handleICATError', () => { error.response.status = 403; handleICATError(error, false); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( 'Test error message (response data)' ); expect(events.length).toBe(1); @@ -149,7 +148,7 @@ describe('handleICATError', () => { error.response.status = 403; handleICATError(error); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( 'Test error message (response data)' ); expect(localStorage.getItem).toBeCalledWith('autoLogin'); @@ -169,7 +168,7 @@ describe('handleICATError', () => { }; handleICATError(error); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( 'Unable to find user by sessionid: null' ); expect(localStorage.getItem).toBeCalledWith('autoLogin'); @@ -197,7 +196,7 @@ describe('handleICATError', () => { error.response.status = 403; handleICATError(error); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( 'Test error message (response data)' ); expect(localStorage.getItem).toBeCalledWith('autoLogin'); @@ -217,7 +216,7 @@ describe('handleICATError', () => { }; handleICATError(error); - expect(log.error).toHaveBeenCalledWith( + expect(logErrorSpy).toHaveBeenCalledWith( 'Unable to find user by sessionid: null' ); expect(localStorage.getItem).toBeCalledWith('autoLogin'); diff --git a/packages/datagateway-common/src/queryClientSettingsUpdater.component.tsx b/packages/datagateway-common/src/queryClientSettingsUpdater.component.tsx index 10b52ce3d..bc6c6abb2 100644 --- a/packages/datagateway-common/src/queryClientSettingsUpdater.component.tsx +++ b/packages/datagateway-common/src/queryClientSettingsUpdater.component.tsx @@ -1,7 +1,48 @@ +import { QueryCache, QueryClient } from '@tanstack/react-query'; +import { AxiosError } from 'axios'; import React from 'react'; -import { QueryClient } from '@tanstack/react-query'; -import { StateType } from './state/app.types'; import { useSelector } from 'react-redux'; +import { handleDOIAPIError } from './api/dois'; +import { LuceneError, handleLuceneError } from './api/lucene'; +import handleICATError from './handleICATError'; +import { StateType } from './state/app.types'; + +declare module '@tanstack/react-query' { + interface Register { + queryMeta: { + icatError?: boolean; + luceneError?: boolean; + DOIAPIError?: boolean; + broadcastCondition?: (error: AxiosError) => boolean; + logCondition?: (error: AxiosError) => boolean; + useEntityErrorHandler?: (error: Error) => void; + }; + } +} + +export const queryCacheConfig: ConstructorParameters[0] = { + onError: (error, query) => { + if (query.meta?.icatError === true) { + const axiosError = error as AxiosError; + handleICATError(axiosError, query.meta?.broadcastCondition?.(axiosError)); + } + if (query.meta?.luceneError === true) { + handleLuceneError(error as AxiosError); + } + if (query.meta?.DOIAPIError === true) { + const axiosError = error as AxiosError<{ + detail: { msg: string }[] | string; + }>; + handleDOIAPIError( + axiosError, + query.meta.logCondition?.(axiosError), + query.meta.broadcastCondition?.(axiosError) + ); + } + if (query.meta?.useEntityErrorHandler) + query.meta.useEntityErrorHandler(error); + }, +}; export const QueryClientSettingsUpdater: React.FC<{ queryRetries: number | undefined; diff --git a/packages/datagateway-common/src/setupTests.tsx b/packages/datagateway-common/src/setupTests.tsx index 1477c8d58..97805a97e 100644 --- a/packages/datagateway-common/src/setupTests.tsx +++ b/packages/datagateway-common/src/setupTests.tsx @@ -1,4 +1,8 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + QueryCache, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; import '@testing-library/jest-dom'; import { History, createMemoryHistory } from 'history'; import React from 'react'; @@ -9,6 +13,7 @@ import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import failOnConsole from 'vitest-fail-on-console'; import { BioPortalTerm } from './app.types'; +import { queryCacheConfig } from './queryClientSettingsUpdater.component'; import type { StateType } from './state/app.types'; import { initialState } from './state/reducers/dgcommon.reducer'; @@ -90,12 +95,7 @@ export const createTestQueryClient = (): QueryClient => retryDelay: 0, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, + queryCache: new QueryCache(queryCacheConfig), }); export const createReactQueryWrapper = ( diff --git a/packages/datagateway-common/src/table/table.component.tsx b/packages/datagateway-common/src/table/table.component.tsx index 9fd1fd59e..6549197cb 100644 --- a/packages/datagateway-common/src/table/table.component.tsx +++ b/packages/datagateway-common/src/table/table.component.tsx @@ -39,6 +39,7 @@ const detailsColumnWidth = 40; const actionsColumnDefaultWidth = 70; const scrollBarHeight = 17; const dataColumnMinWidth = 84; +export const INFINITE_SCROLL_BATCH_SIZE = 25; const StyledTable = styled(Table)(({ theme }) => ({ fontFamily: theme.typography.fontFamily, @@ -398,7 +399,7 @@ export const VirtualizedTable = React.memo( isRowLoaded={isRowLoaded} loadMoreRows={loadMoreRows} rowCount={rowCount} - minimumBatchSize={25} + minimumBatchSize={INFINITE_SCROLL_BATCH_SIZE} > {({ onRowsRendered, registerChild }) => ( { const mockStore = configureStore([thunk]); let state: StateType; @@ -33,18 +31,7 @@ describe('Generic add to cart button', () => { return render( - + diff --git a/packages/datagateway-common/src/views/addToCartButton.component.tsx b/packages/datagateway-common/src/views/addToCartButton.component.tsx index b8974655f..0290de732 100644 --- a/packages/datagateway-common/src/views/addToCartButton.component.tsx +++ b/packages/datagateway-common/src/views/addToCartButton.component.tsx @@ -30,7 +30,7 @@ const AddToCartButton: React.FC = ( (state: StateType) => state.dgcommon.anonUserName ); - const { data: cartItems, isLoading: cartLoading } = useCart(); + const { data: cartItems, isPending: cartLoading } = useCart(); const { mutate: addToCart } = useAddToCart(entityType); const { mutate: removeFromCart } = useRemoveFromCart(entityType); @@ -67,14 +67,14 @@ const AddToCartButton: React.FC = ( disableIfAnon ? t('buttons.disallow_anon_tooltip') : !cartLoading && - !isParentSelected && - typeof selectedIds === 'undefined' - ? t('buttons.cart_loading_failed_tooltip') - : cartLoading - ? t('buttons.cart_loading_tooltip') - : isParentSelected - ? t('buttons.parent_selected_tooltip') - : '' + !isParentSelected && + typeof selectedIds === 'undefined' + ? t('buttons.cart_loading_failed_tooltip') + : cartLoading + ? t('buttons.cart_loading_tooltip') + : isParentSelected + ? t('buttons.parent_selected_tooltip') + : '' } placement="bottom" > diff --git a/packages/datagateway-common/src/views/publishButton.component.tsx b/packages/datagateway-common/src/views/publishButton.component.tsx index c92c865f2..06f0321d4 100644 --- a/packages/datagateway-common/src/views/publishButton.component.tsx +++ b/packages/datagateway-common/src/views/publishButton.component.tsx @@ -36,7 +36,7 @@ const PublishButton: React.FC = (props) => { const isPublishSuccess = status === 'success'; - const isPublishLoading = status === 'loading'; + const isPublishLoading = status === 'pending'; const queryClient = useQueryClient(); diff --git a/packages/datagateway-common/src/views/queueButtons.component.test.tsx b/packages/datagateway-common/src/views/queueButtons.component.test.tsx index 87bb47d3b..54055d7b6 100644 --- a/packages/datagateway-common/src/views/queueButtons.component.test.tsx +++ b/packages/datagateway-common/src/views/queueButtons.component.test.tsx @@ -21,8 +21,6 @@ import { QueueVisitButton, } from './queueButtons.component'; -vi.mock('../handleICATError'); - describe('Queue buttons', () => { const mockStore = configureStore([thunk]); let state: StateType; diff --git a/packages/datagateway-dataview/package.json b/packages/datagateway-dataview/package.json index e788c2af8..b0c16ac07 100644 --- a/packages/datagateway-dataview/package.json +++ b/packages/datagateway-dataview/package.json @@ -8,8 +8,8 @@ "@emotion/styled": "11.14.1", "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", - "@tanstack/react-query": "4.43.0", - "@tanstack/react-query-devtools": "4.43.0", + "@tanstack/react-query": "5.90.21", + "@tanstack/react-query-devtools": "5.91.3", "@types/history": "4.7.11", "@types/jsrsasign": "10.5.2", "@types/lodash.debounce": "4.0.6", @@ -81,6 +81,7 @@ }, "devDependencies": { "@dotenvx/dotenvx": "1.55.1", + "@tanstack/eslint-plugin-query": "5.91.4", "@testing-library/cypress": "10.1.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", diff --git a/packages/datagateway-dataview/src/App.tsx b/packages/datagateway-dataview/src/App.tsx index 93440ff21..1063a7f9d 100644 --- a/packages/datagateway-dataview/src/App.tsx +++ b/packages/datagateway-dataview/src/App.tsx @@ -1,18 +1,26 @@ import { + QueryCache, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { + BroadcastSignOutType, DGCommonMiddleware, DGThemeProvider, - listenToMessages, MicroFrontendId, Preloader, - BroadcastSignOutType, - RequestPluginRerenderType, QueryClientSettingsUpdaterRedux, + RequestPluginRerenderType, + listenToMessages, + queryCacheConfig, } from 'datagateway-common'; import log from 'loglevel'; import React from 'react'; import { Translation } from 'react-i18next'; -import { connect, Provider } from 'react-redux'; -import { AnyAction, applyMiddleware, compose, createStore, Store } from 'redux'; +import { Provider, connect } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { AnyAction, Store, applyMiddleware, compose, createStore } from 'redux'; import { createLogger } from 'redux-logger'; import thunk, { ThunkDispatch } from 'redux-thunk'; import './App.css'; @@ -21,9 +29,6 @@ import PageContainer from './page/pageContainer.component'; import { configureApp } from './state/actions'; import { StateType } from './state/app.types'; import AppReducer from './state/reducers/app.reducer'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { BrowserRouter } from 'react-router-dom'; const middleware = [thunk, DGCommonMiddleware, saveApiUrlMiddleware]; @@ -54,6 +59,7 @@ const queryClient = new QueryClient({ staleTime: 300000, }, }, + queryCache: new QueryCache(queryCacheConfig), }); document.addEventListener(MicroFrontendId, (e) => { diff --git a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx index 00567410f..e9300ba8d 100644 --- a/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx +++ b/packages/datagateway-dataview/src/page/breadcrumbs.component.tsx @@ -6,22 +6,21 @@ import { styled, Typography, } from '@mui/material'; +import { + queryOptions, + useQueries, + UseQueryResult, +} from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { ArrowTooltip, EntityTypes, - handleICATError, parseSearchToQuery, readSciGatewayToken, useRetryICATErrors, } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; -import { - UseQueryResult, - useQueries, - UseQueryOptions, -} from '@tanstack/react-query'; import { useSelector } from 'react-redux'; import { Link, useLocation } from 'react-router-dom'; import { StateType } from '../state/app.types'; @@ -80,7 +79,7 @@ const fetchEntityInformation = async ( const useEntityInformation = ( currentPathnames: string[], landingPageEntities?: string[] -): UseQueryResult<{ displayName: string; url: string }, AxiosError>[] => { +) => { const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const breadcrumbSettings = useSelector( (state: StateType) => state.dgdataview.breadcrumbSettings @@ -88,12 +87,7 @@ const useEntityInformation = ( const retryICATErrors = useRetryICATErrors(); const queryConfigs = React.useMemo(() => { - const queryConfigs: UseQueryOptions< - string, - AxiosError, - { displayName: string; url: string }, - ['entityInfo', string, string] - >[] = []; + const queryConfigs = []; const pathLength = currentPathnames.length; for (let index = 1; index < pathLength; index += 2) { @@ -116,8 +110,8 @@ const useEntityInformation = ( entity === 'investigation' ? 'title' : entity === 'dataPublication' - ? 'title' - : 'name'; + ? 'title' + : 'name'; // this is the field we use to lookup the relevant entity in ICAT - it's usually ID // but for DLS proposals this will be name @@ -170,24 +164,29 @@ const useEntityInformation = ( JSON.stringify({ [requestQueryField]: { eq: entityId } }); } - queryConfigs.push({ - queryKey: ['entityInfo', requestEntityUrl, requestEntityField], - queryFn: () => - fetchEntityInformation( - apiUrl, + queryConfigs.push( + queryOptions({ + queryKey: [ + 'entityInfo', requestEntityUrl, - requestEntityField - ), - onError: (error) => { - handleICATError(error, false); - }, - retry: retryICATErrors, - staleTime: Infinity, - select: (data: string) => ({ - displayName: data, - url: link, - }), - }); + requestEntityField, + apiUrl, + ], + queryFn: () => + fetchEntityInformation( + apiUrl, + requestEntityUrl, + requestEntityField + ), + meta: { icatError: true, broadcastCondition: () => false }, + retry: retryICATErrors, + staleTime: Infinity, + select: (data: string) => ({ + displayName: data, + url: link, + }), + }) + ); } } @@ -200,14 +199,8 @@ const useEntityInformation = ( apiUrl, ]); - // useQueries doesn't allow us to specify type info, so ignore this line - // since we strongly type the queries object anyway - // we also need to prettier-ignore to make sure we don't wrap onto next line - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - // prettier-ignore return useQueries({ - queries: queryConfigs + queries: queryConfigs, }); }; diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx index d796fcef5..a9e5ac4ac 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.test.tsx @@ -135,7 +135,7 @@ describe('PageContainer - Tests', () => { document.body.appendChild(holder); vi.mocked(useQueryClient, { partial: true }).mockReturnValue({ - getQueryData: vi.fn(() => 0), + getQueriesData: vi.fn(() => [[[], 0]] as [[], never][]), }); vi.mocked(axios.get).mockImplementation( @@ -174,7 +174,7 @@ describe('PageContainer - Tests', () => { it('displays the correct entity count', async () => { history.replace(paths.toggle.investigation); vi.mocked(useQueryClient, { partial: true }).mockReturnValue({ - getQueryData: vi.fn(() => 101), + getQueriesData: vi.fn(() => [[[], 101]] as [[], never][]), }); renderComponent(); @@ -381,7 +381,7 @@ describe('PageContainer - Tests', () => { it('do not use StyledRouting component on landing pages', async () => { vi.mocked(checkInstrumentId).mockResolvedValueOnce(true); vi.mocked(useQueryClient, { partial: true }).mockReturnValue({ - getQueryData: vi.fn(), + getQueriesData: vi.fn(), }); history.replace( generatePath(paths.dataPublications.landing.isisDataPublicationLanding, { diff --git a/packages/datagateway-dataview/src/page/pageContainer.component.tsx b/packages/datagateway-dataview/src/page/pageContainer.component.tsx index e2e1a1252..748007f4a 100644 --- a/packages/datagateway-dataview/src/page/pageContainer.component.tsx +++ b/packages/datagateway-dataview/src/page/pageContainer.component.tsx @@ -587,12 +587,13 @@ const DataviewPageContainer: React.FC = () => { // the count can fall behind // eslint-disable-next-line react-hooks/exhaustive-deps React.useEffect(() => { - const count = - queryClient.getQueryData(['count'], { - exact: false, - type: 'active', - }) ?? 0; - if (count !== totalDataCount) setTotalDataCount(count); + const count = (queryClient.getQueriesData({ + queryKey: ['count'], + exact: false, + type: 'active', + }) ?? 0)?.[0]?.[1]; + if (typeof count !== 'undefined' && count !== totalDataCount) + setTotalDataCount(count); }); React.useEffect(() => { @@ -600,7 +601,8 @@ const DataviewPageContainer: React.FC = () => { else setlinearProgressHeight('0px'); }, [loading]); - const isCountFetchingNum = useIsFetching(['count'], { + const isCountFetchingNum = useIsFetching({ + queryKey: ['count'], exact: false, }); const loadedCount = isCountFetchingNum === 0; @@ -698,7 +700,6 @@ const DataviewPageContainer: React.FC = () => { navigateToDownload={navigateToDownload} loggedInAnonymously={loggedInAnonymously} /> - diff --git a/packages/datagateway-dataview/src/page/pageRouting.component.tsx b/packages/datagateway-dataview/src/page/pageRouting.component.tsx index 4bd86a49e..74620f626 100644 --- a/packages/datagateway-dataview/src/page/pageRouting.component.tsx +++ b/packages/datagateway-dataview/src/page/pageRouting.component.tsx @@ -70,11 +70,9 @@ const SafeISISDatafilesTable = (props: { datasetId: string; dataPublication: boolean; }): React.ReactElement => { - const { data, isLoading } = useDataPublication( + const { data, isPending } = useDataPublication( parseInt(props.investigationId), - { - enabled: props.dataPublication, - } + props.dataPublication ); const dataPublicationInvestigationId = data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; @@ -93,7 +91,7 @@ const SafeISISDatafilesTable = (props: { dataPublicationInvestigationId ?? -1, parseInt(props.datasetId) ), - ...(isLoading ? [new Promise(() => undefined)] : []), + ...(isPending ? [new Promise(() => undefined)] : []), ]).then((values) => !values.includes(false)) : Promise.all([ checkInstrumentAndFacilityCycleId( @@ -128,11 +126,9 @@ const SafeISISDatasetLanding = (props: { datasetId: string; dataPublication: boolean; }): React.ReactElement => { - const { data, isLoading } = useDataPublication( + const { data, isPending } = useDataPublication( parseInt(props.investigationId), - { - enabled: props.dataPublication, - } + props.dataPublication ); const dataPublicationInvestigationId = data?.content?.dataCollectionInvestigations?.[0]?.investigation?.id; @@ -151,7 +147,7 @@ const SafeISISDatasetLanding = (props: { dataPublicationInvestigationId ?? -1, parseInt(props.datasetId) ), - ...(isLoading ? [new Promise(() => undefined)] : []), + ...(isPending ? [new Promise(() => undefined)] : []), ]).then((values) => !values.includes(false)) : Promise.all([ checkInstrumentAndFacilityCycleId( @@ -178,11 +174,10 @@ const SafeISISDatasetsTable = (props: { investigationId: string; dataPublication: boolean; }): React.ReactElement => { - const { data, isLoading } = useDataPublication( + const { data, isPending } = useDataPublication( parseInt(props.investigationId), - { - enabled: props.dataPublication, - } + + props.dataPublication ); const dataPublicationInvestigationId = @@ -198,7 +193,7 @@ const SafeISISDatasetsTable = (props: { parseInt(props.instrumentChildId), parseInt(props.investigationId) ), - ...(isLoading ? [new Promise(() => undefined)] : []), + ...(isPending ? [new Promise(() => undefined)] : []), ]).then((values) => !values.includes(false)) : checkInstrumentAndFacilityCycleId( parseInt(props.instrumentId), @@ -225,11 +220,9 @@ const SafeISISDatasetsCardView = (props: { investigationId: string; dataPublication: boolean; }): React.ReactElement => { - const { data, isLoading } = useDataPublication( + const { data, isPending } = useDataPublication( parseInt(props.investigationId), - { - enabled: props.dataPublication, - } + props.dataPublication ); const dataPublicationInvestigationId = @@ -245,7 +238,7 @@ const SafeISISDatasetsCardView = (props: { parseInt(props.instrumentChildId), parseInt(props.investigationId) ), - ...(isLoading ? [new Promise(() => undefined)] : []), + ...(isPending ? [new Promise(() => undefined)] : []), ]).then((values) => !values.includes(false)) : checkInstrumentAndFacilityCycleId( parseInt(props.instrumentId), @@ -311,11 +304,9 @@ const SafeDatafilePreviewer = (props: { datasetId: string; datafileId: string; }): React.ReactElement => { - const { data, isLoading } = useDataPublication( + const { data, isPending } = useDataPublication( parseInt(props.investigationId), - { - enabled: props.dataPublication, - } + props.dataPublication ); const dataPublicationInvestigationId = @@ -336,7 +327,7 @@ const SafeDatafilePreviewer = (props: { parseInt(props.datasetId) ), checkDatasetId(parseInt(props.datasetId), parseInt(props.datafileId)), - ...(isLoading ? [new Promise(() => undefined)] : []), + ...(isPending ? [new Promise(() => undefined)] : []), ]).then((values) => !values.includes(false)) : Promise.all([ checkInstrumentAndFacilityCycleId( diff --git a/packages/datagateway-dataview/src/page/redirect.component.test.tsx b/packages/datagateway-dataview/src/page/redirect.component.test.tsx index b4a7f1941..3dc393df9 100644 --- a/packages/datagateway-dataview/src/page/redirect.component.test.tsx +++ b/packages/datagateway-dataview/src/page/redirect.component.test.tsx @@ -79,17 +79,17 @@ describe('Redirect component', () => { if (entityName === 'investigation') return { data: mockInvestigationData, - isLoading: false, + isPending: false, }; if (entityName === 'dataset') return { data: mockDatasetData, - isLoading: false, + isPending: false, }; if (entityName === 'datafile') return { data: mockDatafileData, - isLoading: false, + isPending: false, }; else return {}; }); @@ -128,7 +128,7 @@ describe('Redirect component', () => { it('displays loading spinner when things are loading', async () => { vi.mocked(useEntity, { partial: true }).mockReturnValue({ data: undefined, - isLoading: true, + isPending: true, }); renderComponent(); @@ -145,7 +145,7 @@ describe('Redirect component', () => { }; vi.mocked(useEntity, { partial: true }).mockReturnValue({ data: undefined, - isLoading: false, + isPending: false, }); renderComponent(); @@ -261,7 +261,7 @@ describe('Redirect component', () => { history.replace('/redirect/ISIS/datafile/name/3'); vi.mocked(useEntity, { partial: true }).mockReturnValue({ data: undefined, - isLoading: true, + isPending: true, }); renderComponent(); @@ -293,7 +293,7 @@ describe('Redirect component', () => { }; vi.mocked(useEntity, { partial: true }).mockReturnValue({ data: undefined, - isLoading: false, + isPending: false, }); renderComponent(); @@ -335,7 +335,7 @@ describe('Redirect component', () => { }; vi.mocked(useEntity, { partial: true }).mockReturnValue({ data: undefined, - isLoading: false, + isPending: false, }); renderComponent(); diff --git a/packages/datagateway-dataview/src/page/redirect.component.tsx b/packages/datagateway-dataview/src/page/redirect.component.tsx index 53d9d1894..07be03a41 100644 --- a/packages/datagateway-dataview/src/page/redirect.component.tsx +++ b/packages/datagateway-dataview/src/page/redirect.component.tsx @@ -53,7 +53,7 @@ type DoiRedirectRouteParams = { export const DoiRedirect: React.FC = () => { const { entityName, entityId } = useParams(); - const { data: investigation, isLoading: isInvestigationLoading } = useEntity( + const { data: investigation, isPending: isInvestigationLoading } = useEntity( 'investigation', 'id', entityId, @@ -102,7 +102,7 @@ export const GenericRedirect: React.FC = () => { const isISIS = facilityName.toLowerCase() === FACILITY_NAME.isis.toLowerCase(); - const { data: entity, isLoading: isEntityLoading } = useEntity( + const { data: entity, isPending: isEntityLoading } = useEntity( entityName, entityField, decodeURIComponent(fieldValue), // call decodeURIComponent here to e.g. allow URL encoding of slashes to search for datafile locations etc. diff --git a/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx index c4076a6f4..5dba66b76 100644 --- a/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/datasetCardView.component.test.tsx @@ -1,25 +1,25 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + render, + screen, + within, + type RenderResult, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { - type Dataset, dGCommonInitialState, useDatasetCount, useDatasetsPaginated, + type Dataset, } from 'datagateway-common'; +import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; import type { StateType } from '../../state/app.types'; -import DatasetCardView from './datasetCardView.component'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { createMemoryHistory, type History } from 'history'; import { initialState as dgDataViewInitialState } from '../../state/reducers/dgdataview.reducer'; -import { - render, - type RenderResult, - screen, - within, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import DatasetCardView from './datasetCardView.component'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -74,11 +74,11 @@ describe('Dataset - Card View', () => { vi.mocked(useDatasetCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useDatasetsPaginated, { partial: true }).mockReturnValue({ data: cardData, - isLoading: false, + isPending: false, }); window.scrollTo = vi.fn(); diff --git a/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx b/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx index e73e6710e..9750a381c 100644 --- a/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/datasetCardView.component.tsx @@ -1,21 +1,21 @@ -import React from 'react'; -import ConfirmationNumber from '@mui/icons-material/ConfirmationNumber'; import CalendarToday from '@mui/icons-material/CalendarToday'; +import ConfirmationNumber from '@mui/icons-material/ConfirmationNumber'; import { + AddToCartButton, CardView, Dataset, datasetLink, parseSearchToQuery, - useDateFilter, useDatasetCount, useDatasetsPaginated, + useDateFilter, usePushFilter, usePushPage, usePushResults, useSort, useTextFilter, - AddToCartButton, } from 'datagateway-common'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -41,7 +41,7 @@ const DatasetCardView = (props: DatasetCardViewProps): React.ReactElement => { const pushPage = usePushPage(); const pushResults = usePushResults(); - const { data: totalDataCount, isLoading: countLoading } = useDatasetCount([ + const { data: totalDataCount, isPending: countLoading } = useDatasetCount([ { filterType: 'where', filterValue: JSON.stringify({ @@ -49,7 +49,7 @@ const DatasetCardView = (props: DatasetCardViewProps): React.ReactElement => { }), }, ]); - const { isLoading: dataLoading, data } = useDatasetsPaginated([ + const { isPending: dataLoading, data } = useDatasetsPaginated([ { filterType: 'where', filterValue: JSON.stringify({ diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx index fa49c71bf..9f3770dca 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.test.tsx @@ -1,9 +1,14 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { RenderResult, render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios, { AxiosResponse } from 'axios'; import { Dataset, dGCommonInitialState, useDatasetCount, useDatasetsPaginated, } from 'datagateway-common'; +import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; @@ -11,11 +16,6 @@ import thunk from 'redux-thunk'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import DLSDatasetsCardView from './dlsDatasetsCardView.component'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { createMemoryHistory, History } from 'history'; -import { render, RenderResult, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import axios, { AxiosResponse } from 'axios'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -69,11 +69,11 @@ describe('DLS Datasets - Card View', () => { vi.mocked(useDatasetCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useDatasetsPaginated, { partial: true }).mockReturnValue({ data: cardData, - isLoading: false, + isPending: false, }); axios.get = vi diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx index c67d9dc22..293e7f22e 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsDatasetsCardView.component.tsx @@ -1,24 +1,24 @@ -import React from 'react'; +import CalendarToday from '@mui/icons-material/CalendarToday'; +import Save from '@mui/icons-material/Save'; import { + AddToCartButton, CardView, CardViewDetails, + DLSDatasetDetailsPanel, Dataset, - tableLink, + formatBytes, parseSearchToQuery, - useDateFilter, + tableLink, useDatasetCount, useDatasetsPaginated, + useDateFilter, usePushFilter, usePushPage, usePushResults, useSort, useTextFilter, - AddToCartButton, - DLSDatasetDetailsPanel, - formatBytes, } from 'datagateway-common'; -import CalendarToday from '@mui/icons-material/CalendarToday'; -import Save from '@mui/icons-material/Save'; +import React from 'react'; import ConfirmationNumberIcon from '@mui/icons-material/ConfirmationNumber'; import { useTranslation } from 'react-i18next'; @@ -55,7 +55,7 @@ const DLSDatasetsCardView = (props: DLSDatasetsCVProps): React.ReactElement => { setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = useDatasetCount([ + const { data: totalDataCount, isPending: countLoading } = useDatasetCount([ { filterType: 'where', filterValue: JSON.stringify({ @@ -63,7 +63,7 @@ const DLSDatasetsCardView = (props: DLSDatasetsCVProps): React.ReactElement => { }), }, ]); - const { data, isLoading: dataLoading } = useDatasetsPaginated( + const { data, isPending: dataLoading } = useDatasetsPaginated( [ { filterType: 'where', diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx index f049ee6bd..9b8f2e881 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.test.tsx @@ -1,9 +1,13 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, screen, type RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { dGCommonInitialState, - type Investigation, useInvestigationCount, useInvestigationsPaginated, + type Investigation, } from 'datagateway-common'; +import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; @@ -11,10 +15,6 @@ import thunk from 'redux-thunk'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import DLSProposalsCardView from './dlsProposalsCardView.component'; -import { createMemoryHistory, type History } from 'history'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, type RenderResult, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -66,11 +66,11 @@ describe('DLS Proposals - Card View', () => { vi.mocked(useInvestigationCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useInvestigationsPaginated, { partial: true }).mockReturnValue({ data: cardData, - isLoading: false, + isPending: false, }); // Prevent error logging diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx index 33cb01cd9..2c766c51f 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsProposalsCardView.component.tsx @@ -1,10 +1,9 @@ -import React from 'react'; import { CardView, CardViewDetails, Investigation, - tableLink, parseSearchToQuery, + tableLink, useInvestigationCount, useInvestigationsPaginated, usePushFilter, @@ -13,6 +12,7 @@ import { useSort, useTextFilter, } from 'datagateway-common'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -39,14 +39,14 @@ const DLSProposalsCardView = (): React.ReactElement => { setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = + const { data: totalDataCount, isPending: countLoading } = useInvestigationCount([ { filterType: 'distinct', filterValue: JSON.stringify(['name', 'title']), }, ]); - const { isLoading: dataLoading, data } = useInvestigationsPaginated( + const { isPending: dataLoading, data } = useInvestigationsPaginated( [ { filterType: 'distinct', diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx index e09f216a1..42f149d8d 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.test.tsx @@ -69,11 +69,11 @@ describe('DLS Visits - Card View', () => { vi.mocked(useInvestigationCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useInvestigationsPaginated, { partial: true }).mockReturnValue({ data: cardData, - isLoading: false, + isPending: false, }); axios.get = vi diff --git a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx index 048c8671f..aa4c3881f 100644 --- a/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/dls/dlsVisitsCardView.component.tsx @@ -1,11 +1,19 @@ import React from 'react'; +import Assessment from '@mui/icons-material/Assessment'; +import CalendarToday from '@mui/icons-material/CalendarToday'; +import Save from '@mui/icons-material/Save'; +import { Typography } from '@mui/material'; import { + ArrowTooltip, CardView, CardViewDetails, + DLSVisitDetailsPanel, Investigation, - tableLink, + formatBytes, + nestedValue, parseSearchToQuery, + tableLink, useDateFilter, useInvestigationCount, useInvestigationsPaginated, @@ -14,17 +22,9 @@ import { usePushResults, useSort, useTextFilter, - nestedValue, - ArrowTooltip, - DLSVisitDetailsPanel, - formatBytes, } from 'datagateway-common'; -import Assessment from '@mui/icons-material/Assessment'; -import CalendarToday from '@mui/icons-material/CalendarToday'; -import Save from '@mui/icons-material/Save'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { Typography } from '@mui/material'; interface DLSVisitsCVProps { proposalName: string; @@ -56,14 +56,14 @@ const DLSVisitsCardView = (props: DLSVisitsCVProps): React.ReactElement => { setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = + const { data: totalDataCount, isPending: countLoading } = useInvestigationCount([ { filterType: 'where', filterValue: JSON.stringify({ name: { eq: proposalName } }), }, ]); - const { isLoading: dataLoading, data } = useInvestigationsPaginated( + const { isPending: dataLoading, data } = useInvestigationsPaginated( [ { filterType: 'where', diff --git a/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx b/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx index e78b4f687..cd3a2dfc5 100644 --- a/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/investigationCardView.component.tsx @@ -4,14 +4,16 @@ import Public from '@mui/icons-material/Public'; import Save from '@mui/icons-material/Save'; import { Link as MuiLink } from '@mui/material'; import { + AddToCartButton, CardView, - formatFilterCount, Investigation, + formatBytes, + formatFilterCount, investigationLink, parseSearchToQuery, - useDateFilter, useCustomFilter, useCustomFilterCount, + useDateFilter, useInvestigationCount, useInvestigationsPaginated, usePushFilter, @@ -19,8 +21,6 @@ import { usePushResults, useSort, useTextFilter, - AddToCartButton, - formatBytes, } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -42,10 +42,10 @@ const InvestigationCardView = (): React.ReactElement => { const pushPage = usePushPage(); const pushResults = usePushResults(); - const { data: totalDataCount, isLoading: countLoading } = + const { data: totalDataCount, isPending: countLoading } = useInvestigationCount(); - const { isLoading: dataLoading, data } = useInvestigationsPaginated([ + const { isPending: dataLoading, data } = useInvestigationsPaginated([ { filterType: 'include', filterValue: JSON.stringify('type'), diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx index e3f49cc3d..c01e86adf 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDataPublicationsCardView.component.tsx @@ -1,24 +1,24 @@ -import React from 'react'; +import CalendarToday from '@mui/icons-material/CalendarToday'; +import Public from '@mui/icons-material/Public'; +import { Link as MuiLink } from '@mui/material'; import { CardView, CardViewDetails, - parseSearchToQuery, DataPublication, + parseSearchToQuery, tableLink, + useDataPublicationCount, + useDataPublicationsPaginated, useDateFilter, usePushFilter, usePushPage, usePushResults, useSort, - useDataPublicationsPaginated, - useDataPublicationCount, useTextFilter, } from 'datagateway-common'; -import CalendarToday from '@mui/icons-material/CalendarToday'; -import Public from '@mui/icons-material/Public'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; -import { Link as MuiLink } from '@mui/material'; interface ISISDataPublicationsCVProps { instrumentId: string; @@ -53,7 +53,7 @@ const ISISDataPublicationsCardView = ( setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = + const { data: totalDataCount, isPending: countLoading } = useDataPublicationCount([ { filterType: 'where', @@ -96,7 +96,7 @@ const ISISDataPublicationsCardView = ( ]), ]); - const { isLoading: dataLoading, data } = useDataPublicationsPaginated( + const { isPending: dataLoading, data } = useDataPublicationsPaginated( [ { filterType: 'where', diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx index c3832a4d0..f09e8b61d 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.test.tsx @@ -1,22 +1,22 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, screen, type RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios, { AxiosResponse } from 'axios'; import { - type Dataset, dGCommonInitialState, useDatasetCount, useDatasetsPaginated, + type Dataset, } from 'datagateway-common'; +import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import { generatePath, Router } from 'react-router-dom'; +import { Router, generatePath } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import { paths } from '../../../page/pageContainer.component'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISDatasetsCardView from './isisDatasetsCardView.component'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { createMemoryHistory, type History } from 'history'; -import { render, type RenderResult, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import { paths } from '../../../page/pageContainer.component'; -import axios, { AxiosResponse } from 'axios'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -77,11 +77,11 @@ describe('ISIS Datasets - Card View', () => { vi.mocked(useDatasetCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useDatasetsPaginated, { partial: true }).mockReturnValue({ data: cardData, - isLoading: false, + isPending: false, }); axios.get = vi diff --git a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx index 2a4342861..bf852417a 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisDatasetsCardView.component.tsx @@ -1,28 +1,28 @@ -import React from 'react'; +import CalendarToday from '@mui/icons-material/CalendarToday'; +import Save from '@mui/icons-material/Save'; +import { styled } from '@mui/material'; import { + AddToCartButton, CardView, CardViewDetails, Dataset, - tableLink, + DownloadButton, + ISISDatasetDetailsPanel, formatBytes, parseSearchToQuery, - useDateFilter, + tableLink, useDatasetCount, useDatasetsPaginated, + useDateFilter, usePushFilter, usePushPage, usePushResults, useSort, useTextFilter, - AddToCartButton, - DownloadButton, - ISISDatasetDetailsPanel, } from 'datagateway-common'; -import CalendarToday from '@mui/icons-material/CalendarToday'; -import Save from '@mui/icons-material/Save'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useHistory, useLocation } from 'react-router-dom'; -import { styled } from '@mui/material'; const ActionButtonsContainer = styled('div')(({ theme }) => ({ display: 'flex', @@ -66,7 +66,7 @@ const ISISDatasetsCardView = ( setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = useDatasetCount([ + const { data: totalDataCount, isPending: countLoading } = useDatasetCount([ { filterType: 'where', filterValue: JSON.stringify({ @@ -74,7 +74,7 @@ const ISISDatasetsCardView = ( }), }, ]); - const { data, isLoading: dataLoading } = useDatasetsPaginated( + const { data, isPending: dataLoading } = useDatasetsPaginated( [ { filterType: 'where', diff --git a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx index 005781221..6df431e38 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.test.tsx @@ -1,21 +1,21 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, RenderResult, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { dGCommonInitialState, FacilityCycle, useFacilityCycleCount, useFacilityCyclesPaginated, } from 'datagateway-common'; +import { createMemoryHistory, History } from 'history'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; +import type { MockInstance } from 'vitest'; import { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISFacilityCyclesCardView from './isisFacilityCyclesCardView.component'; -import { createMemoryHistory, History } from 'history'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, RenderResult, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import type { MockInstance } from 'vitest'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -67,11 +67,11 @@ describe('ISIS Facility Cycles - Card View', () => { vi.mocked(useFacilityCycleCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useFacilityCyclesPaginated, { partial: true }).mockReturnValue({ data: cardData, - isLoading: false, + isPending: false, }); // Prevent error logging diff --git a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx index b72fc7de1..ed4c72bdb 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisFacilityCyclesCardView.component.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import CalendarToday from '@mui/icons-material/CalendarToday'; import { CardView, CardViewDetails, @@ -14,7 +14,7 @@ import { useSort, useTextFilter, } from 'datagateway-common'; -import CalendarToday from '@mui/icons-material/CalendarToday'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router-dom'; @@ -49,9 +49,9 @@ const ISISFacilityCyclesCardView = ( setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = + const { data: totalDataCount, isPending: countLoading } = useFacilityCycleCount(parseInt(instrumentId)); - const { isLoading: dataLoading, data } = useFacilityCyclesPaginated( + const { isPending: dataLoading, data } = useFacilityCyclesPaginated( parseInt(instrumentId), isMounted ); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx index b4fd0a50a..3c1ce3416 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.test.tsx @@ -1,9 +1,14 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render, screen, type RenderResult } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios, { AxiosResponse } from 'axios'; import { dGCommonInitialState, - type Instrument, useInstrumentCount, useInstrumentsPaginated, + type Instrument, } from 'datagateway-common'; +import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; @@ -11,11 +16,6 @@ import thunk from 'redux-thunk'; import type { StateType } from '../../../state/app.types'; import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; import ISISInstrumentsCardView from './isisInstrumentsCardView.component'; -import { createMemoryHistory, type History } from 'history'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { render, type RenderResult, screen } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import axios, { AxiosResponse } from 'axios'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -65,11 +65,11 @@ describe('ISIS Instruments - Card View', () => { vi.mocked(useInstrumentCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useInstrumentsPaginated, { partial: true }).mockReturnValue({ data: cardData, - isLoading: false, + isPending: false, }); axios.get = vi diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx index e43734cf4..3b44b2a4f 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInstrumentsCardView.component.tsx @@ -1,9 +1,10 @@ -import { Link } from '@mui/material'; import LinkIcon from '@mui/icons-material/Link'; import Title from '@mui/icons-material/Title'; +import { Link } from '@mui/material'; import { CardView, CardViewDetails, + ISISInstrumentDetailsPanel, Instrument, parseSearchToQuery, tableLink, @@ -14,7 +15,6 @@ import { usePushResults, useSort, useTextFilter, - ISISInstrumentDetailsPanel, } from 'datagateway-common'; import React from 'react'; import { useTranslation } from 'react-i18next'; @@ -50,9 +50,9 @@ const ISISInstrumentsCardView = ( setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = + const { data: totalDataCount, isPending: countLoading } = useInstrumentCount(); - const { isLoading: dataLoading, data } = useInstrumentsPaginated( + const { isPending: dataLoading, data } = useInstrumentsPaginated( undefined, isMounted ); diff --git a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx index e9733e0cb..c898289bb 100644 --- a/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx +++ b/packages/datagateway-dataview/src/views/card/isis/isisInvestigationsCardView.component.tsx @@ -100,9 +100,9 @@ const ISISInvestigationsCardView = ( setIsMounted(true); }, []); - const { data: totalDataCount, isLoading: countLoading } = + const { data: totalDataCount, isPending: countLoading } = useInvestigationCount(investigationQueryFilters); - const { data, isLoading: dataLoading } = useInvestigationsPaginated( + const { data, isPending: dataLoading } = useInvestigationsPaginated( [ ...investigationQueryFilters, { diff --git a/packages/datagateway-dataview/src/views/citationFormatter.component.tsx b/packages/datagateway-dataview/src/views/citationFormatter.component.tsx index 04f51e46c..22a885a8a 100644 --- a/packages/datagateway-dataview/src/views/citationFormatter.component.tsx +++ b/packages/datagateway-dataview/src/views/citationFormatter.component.tsx @@ -11,7 +11,7 @@ import { Typography, } from '@mui/material'; import { useQuery, UseQueryResult } from '@tanstack/react-query'; -import axios, { AxiosError } from 'axios'; +import axios from 'axios'; import { Mark } from 'datagateway-common'; import React from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -57,9 +57,18 @@ const useCitation = ( ): UseQueryResult => { const { doi, formattedUsers, title, startDate } = citationProps; - return useQuery( - [formattedUsers, title, startDate, publisherName, doi, format, locale], - () => { + return useQuery({ + queryKey: [ + formattedUsers, + title, + startDate, + publisherName, + doi, + format, + locale, + ], + + queryFn: () => { //Default citation format (No use of DataCite) if (format === 'default') { let citation = ''; @@ -78,10 +87,7 @@ const useCitation = ( else throw new Error('No DOI was supplied'); } }, - { - cacheTime: Infinity, - } - ); + }); }; const CitationFormatter = ( diff --git a/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx b/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx index a274fa8ec..7385cb354 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.test.tsx @@ -44,12 +44,6 @@ function createQueryClient(): QueryClient { retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); } diff --git a/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.tsx b/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.tsx index 9e8721225..2b8a24fb9 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/datafilePreviewer.component.tsx @@ -108,11 +108,9 @@ function DatafilePreviewer({ const { data: datafile, - isInitialLoading: isLoadingMetadata, + isLoading: isLoadingMetadata, error: loadDatafileMetaError, - } = useDatafileDetails(datafileId, undefined, { - enabled: !Number.isNaN(datafileId), - }); + } = useDatafileDetails(datafileId, undefined, !Number.isNaN(datafileId)); const datafileExtension = datafile && extensionOf(datafile); const supportsExtension = diff --git a/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.test.tsx b/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.test.tsx index 7d45def78..004806052 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.test.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.test.tsx @@ -1,9 +1,9 @@ +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import type { RenderResult } from '@testing-library/react'; import { act, render, screen, waitFor } from '@testing-library/react'; import { DGThemeProvider } from 'datagateway-common'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { Provider } from 'react-redux'; -import { combineReducers, createStore, Store } from 'redux'; +import { Store, combineReducers, createStore } from 'redux'; import { StateType } from '../../../state/app.types'; import dgdataviewReducer from '../../../state/reducers/dgdataview.reducer'; import { @@ -21,12 +21,6 @@ function renderComponent(store: Store): RenderResult { client={ new QueryClient({ defaultOptions: { queries: { retry: false } }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }) } > diff --git a/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.tsx b/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.tsx index 0c7a14e55..ef286d154 100644 --- a/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.tsx +++ b/packages/datagateway-dataview/src/views/datafilePreview/previewComponents/textPreview.component.tsx @@ -1,6 +1,6 @@ import { styled, Typography } from '@mui/material'; -import { useTranslation } from 'react-i18next'; import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; import { useSelector } from 'react-redux'; import { StateType } from '../../../state/app.types'; import type { PreviewComponentProps } from './previewComponents'; @@ -64,12 +64,14 @@ function TextPreview({ ); const [t] = useTranslation(); const { - isLoading: isReadingContent, + isPending: isReadingContent, isError: isReadContentError, data: textContent, - } = useQuery(['datafile', datafile.id, 'textContent'], () => - datafileContent.text() - ); + } = useQuery({ + queryKey: ['datafile', datafile.id, 'textContent'], + + queryFn: () => datafileContent.text(), + }); if (isReadingContent) { return ( diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx index a3a4b3346..82abf0610 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.test.tsx @@ -214,8 +214,6 @@ describe('DataPublication content table component', () => { it('renders investigations correctly', async () => { renderComponent(); - expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); - let rows: HTMLElement[] = []; await waitFor(async () => { rows = await findAllRows(); @@ -225,6 +223,8 @@ describe('DataPublication content table component', () => { const row = rows[0]; + expect(screen.queryByRole('checkbox')).not.toBeInTheDocument(); + expect( await findColumnHeaderByName('investigations.visit_id') ).toBeInTheDocument(); diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx index 57891891d..103414f28 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationContentTable.component.tsx @@ -68,10 +68,10 @@ const DLSDataPublicationContentTable = ( const dateFilter = useDateFilter(filters); const handleSort = useSort(); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart(currentTab); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart(currentTab); const { data: totalDataCount } = useDataPublicationContentCount( @@ -85,7 +85,7 @@ const DLSDataPublicationContentTable = ( ); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx index 388181a86..bfcf68174 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.test.tsx @@ -36,11 +36,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); describe('DOI edit form component', () => { diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx index 25ac25638..c991e003a 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationEditForm.component.tsx @@ -98,7 +98,7 @@ const DLSDataPublicationEditForm: React.FC = ( }, { filterType: 'order', filterValue: JSON.stringify('createTime desc') }, ], - { enabled: !!dataPublication?.pid } + !!dataPublication?.pid ); const versionDataPublication = versionDataPublications?.[0]; @@ -220,7 +220,7 @@ const DLSDataPublicationEditForm: React.FC = ( useDeleteDraftVersion(); const { data: cart } = useCart(); - const { isLoading: cartMintabilityLoading, error: mintableError } = + const { isPending: cartMintabilityLoading, error: mintableError } = useIsCartMintable(cart, doiMinterUrl); const unmintableEntityIDs: number[] | undefined = React.useMemo( @@ -376,8 +376,8 @@ const DLSDataPublicationEditForm: React.FC = ( draftMetadata={mintDraftVersionData?.version.attributes} onBackClick={handleBackClick} onConfirmClick={handleConfirmClick} - deleteLoading={deleteVersionDraftStatus === 'loading'} - publishLoading={publishingVersionStatus === 'loading'} + deleteLoading={deleteVersionDraftStatus === 'pending'} + publishLoading={publishingVersionStatus === 'pending'} /> ) : ( @@ -426,7 +426,7 @@ const DLSDataPublicationEditForm: React.FC = ( subjects={subjects} setSubjects={setSubjects} disableMintButton={false} - mintLoading={mintDraftVersionStatus === 'loading'} + mintLoading={mintDraftVersionStatus === 'pending'} onMintClick={handleMintClick} /> diff --git a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx index b28280651..a0d15cc57 100644 --- a/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx +++ b/packages/datagateway-dataview/src/views/landing/dls/dlsDataPublicationLanding.component.tsx @@ -145,9 +145,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { ); const { dataPublicationId } = props; - const { data, isInitialLoading } = useDataPublication( - parseInt(dataPublicationId) - ); + const { data, isPending } = useDataPublication(parseInt(dataPublicationId)); const { data: dataciteData } = useDOI(data?.pid); const [techniques, subjects] = React.useMemo( @@ -471,7 +469,7 @@ const LandingPage = (props: LandingPageProps): React.ReactElement => { }} data-testid="dls-dataPublication-landing" > - {isInitialLoading ? ( + {isPending ? (
diff --git a/packages/datagateway-dataview/src/views/roleSelector.component.tsx b/packages/datagateway-dataview/src/views/roleSelector.component.tsx index 869475a5e..a17770779 100644 --- a/packages/datagateway-dataview/src/views/roleSelector.component.tsx +++ b/packages/datagateway-dataview/src/views/roleSelector.component.tsx @@ -5,14 +5,13 @@ import { Select, SelectChangeEvent, } from '@mui/material'; -import { useQuery, UseQueryResult } from '@tanstack/react-query'; +import { UseQueryResult, useQuery } from '@tanstack/react-query'; import axios, { AxiosError } from 'axios'; import { - handleICATError, InvestigationUser, + StateType, parseSearchToQuery, readSciGatewayToken, - StateType, usePushFilter, useRetryICATErrors, } from 'datagateway-common'; @@ -50,16 +49,12 @@ export const useRoles = ( const apiUrl = useSelector((state: StateType) => state.dgcommon.urls.apiUrl); const retryICATErrors = useRetryICATErrors(); - return useQuery( - ['roles', username], - () => fetchRoles(apiUrl, username), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - } - ); + return useQuery({ + queryKey: ['roles', username, apiUrl], + queryFn: () => fetchRoles(apiUrl, username), + meta: { icatError: true }, + retry: retryICATErrors, + }); }; const RoleSelector: React.FC = () => { diff --git a/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx b/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx index de0716c75..aafa2dc05 100644 --- a/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/datafileTable.component.tsx @@ -52,7 +52,7 @@ const DatafileTable = (props: DatafileTableProps): React.ReactElement => { const textFilter = useTextFilter(filters); const dateFilter = useDateFilter(filters); const handleSort = useSort(); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'datafile', [ { @@ -62,10 +62,10 @@ const DatafileTable = (props: DatafileTableProps): React.ReactElement => { ], !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('datafile'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('datafile'); const { data: totalDataCount } = useDatafileCount([ @@ -83,7 +83,7 @@ const DatafileTable = (props: DatafileTableProps): React.ReactElement => { ]); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx b/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx index d3d46d243..5633b9ce6 100644 --- a/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/datasetTable.component.tsx @@ -49,7 +49,7 @@ const DatasetTable = (props: DatasetTableProps): React.ReactElement => { const dateFilter = useDateFilter(filters); const handleSort = useSort(); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'dataset', [ { @@ -61,10 +61,10 @@ const DatasetTable = (props: DatasetTableProps): React.ReactElement => { ], !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('dataset'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('dataset'); const { data: totalDataCount } = useDatasetCount([ @@ -86,7 +86,7 @@ const DatasetTable = (props: DatasetTableProps): React.ReactElement => { ]); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx index 0d65248c4..7916a50ef 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDOITables.component.tsx @@ -63,7 +63,7 @@ const DLSBaseDOIsTable = (props: DLSBaseDOIsTableProps): React.ReactElement => { const handleSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); @@ -161,116 +161,120 @@ export const DLSMyDOIsTable = (): React.ReactElement => { }, ] : doiType === 'openSession' - ? [ - { - filterType: 'where', - filterValue: JSON.stringify({ - 'users.user.name': { eq: username }, - }), - }, - { - filterType: 'where', - filterValue: JSON.stringify({ - 'type.name': { eq: 'Investigation' }, - }), - }, - // TODO: add back in when we can query for is not null - // { - // filterType: 'where', - // filterValue: JSON.stringify({ - // publicationDate: { neq: null }, - // }), - // }, - ] - : doiType === 'closedSession' - ? [ - { - filterType: 'where', - filterValue: JSON.stringify({ - 'users.user.name': { eq: username }, - }), - }, - { - filterType: 'where', - filterValue: JSON.stringify({ - 'type.name': { eq: 'Investigation' }, - }), - }, - // TODO: add back in when we can query for is null - // { - // filterType: 'where', - // filterValue: JSON.stringify({ - // publicationDate: { eq: null }, - // }), - // }, - ] - : doiType === 'user' - ? [ - { - filterType: 'where', - filterValue: JSON.stringify({ - 'users.user.name': { eq: username }, - }), - }, - { - filterType: 'where', - filterValue: JSON.stringify({ - 'relatedItems.relationType': { eq: DOIRelationType.HasVersion }, - }), - }, - { - filterType: 'distinct', - filterValue: JSON.stringify([ - 'id', - 'title', - 'pid', - 'publicationDate', - ]), - }, - { - filterType: 'where', - filterValue: JSON.stringify({ - 'type.name': { eq: 'User-defined' }, - }), - }, - ] - : [ - { - filterType: 'where', - filterValue: JSON.stringify({ - 'users.user.name': { eq: username }, - }), - }, - { - filterType: 'where', - filterValue: JSON.stringify({ - 'users.contributorType': { - eq: ContributorType.Minter, + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'users.user.name': { eq: username }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'Investigation' }, + }), + }, + // TODO: add back in when we can query for is not null + // { + // filterType: 'where', + // filterValue: JSON.stringify({ + // publicationDate: { neq: null }, + // }), + // }, + ] + : doiType === 'closedSession' + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'users.user.name': { eq: username }, + }), }, - }), - }, - { - filterType: 'where', - filterValue: JSON.stringify({ - 'relatedItems.relationType': { eq: DOIRelationType.HasVersion }, - }), - }, - { - filterType: 'distinct', - filterValue: JSON.stringify([ - 'id', - 'title', - 'pid', - 'publicationDate', - ]), - }, - { - filterType: 'where', - filterValue: JSON.stringify({ - 'type.name': { eq: 'User-defined' }, - }), - }, - ]; + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'Investigation' }, + }), + }, + // TODO: add back in when we can query for is null + // { + // filterType: 'where', + // filterValue: JSON.stringify({ + // publicationDate: { eq: null }, + // }), + // }, + ] + : doiType === 'user' + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'users.user.name': { eq: username }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'relatedItems.relationType': { + eq: DOIRelationType.HasVersion, + }, + }), + }, + { + filterType: 'distinct', + filterValue: JSON.stringify([ + 'id', + 'title', + 'pid', + 'publicationDate', + ]), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'User-defined' }, + }), + }, + ] + : [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'users.user.name': { eq: username }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'users.contributorType': { + eq: ContributorType.Minter, + }, + }), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'relatedItems.relationType': { + eq: DOIRelationType.HasVersion, + }, + }), + }, + { + filterType: 'distinct', + filterValue: JSON.stringify([ + 'id', + 'title', + 'pid', + 'publicationDate', + ]), + }, + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'User-defined' }, + }), + }, + ]; return ; }; @@ -309,44 +313,44 @@ export const DLSAllDOIsTable = (): React.ReactElement => { }, ] : doiType === 'openSession' - ? [ - { - filterType: 'where', - filterValue: JSON.stringify({ - 'type.name': { eq: 'Investigation' }, - }), - }, - // TODO: add back in when we can query for is not null - // { - // filterType: 'where', - // filterValue: JSON.stringify({ - // publicationDate: { neq: null }, - // }), - // }, - ] - : doiType === 'closedSession' - ? [ - { - filterType: 'where', - filterValue: JSON.stringify({ - 'type.name': { eq: 'Investigation' }, - }), - }, - // TODO: add back in when we can query for is null - // { - // filterType: 'where', - // filterValue: JSON.stringify({ - // publicationDate: { eq: null }, - // }), - // }, - ] - : [ - { - filterType: 'where', - filterValue: JSON.stringify({ - 'type.name': { eq: 'Investigation' }, - }), - }, - ]; + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'Investigation' }, + }), + }, + // TODO: add back in when we can query for is not null + // { + // filterType: 'where', + // filterValue: JSON.stringify({ + // publicationDate: { neq: null }, + // }), + // }, + ] + : doiType === 'closedSession' + ? [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'Investigation' }, + }), + }, + // TODO: add back in when we can query for is null + // { + // filterType: 'where', + // filterValue: JSON.stringify({ + // publicationDate: { eq: null }, + // }), + // }, + ] + : [ + { + filterType: 'where', + filterValue: JSON.stringify({ + 'type.name': { eq: 'Investigation' }, + }), + }, + ]; return ; }; diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx index fa7d1d507..a1eeb30e0 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.test.tsx @@ -93,7 +93,7 @@ describe('DLS datafiles table component', () => { vi.mocked(useCart, { partial: true }).mockReturnValue({ data: [], - isLoading: false, + isPending: false, }); vi.mocked(useDatafileCount, { partial: true }).mockReturnValue({ data: 0, @@ -104,15 +104,15 @@ describe('DLS datafiles table component', () => { }); vi.mocked(useIds, { partial: true }).mockReturnValue({ data: [1], - isLoading: false, + isPending: false, }); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); axios.get = vi @@ -273,7 +273,7 @@ describe('DLS datafiles table component', () => { const addToCart = vi.fn(); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: addToCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -295,13 +295,13 @@ describe('DLS datafiles table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); const removeFromCart = vi.fn(); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: removeFromCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -331,7 +331,7 @@ describe('DLS datafiles table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); renderComponent(); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx index 5df497a2b..0922a31ff 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatafilesTable.component.tsx @@ -53,7 +53,7 @@ const DLSDatafilesTable = ( const dateFilter = useDateFilter(filters); const handleSort = useSort(); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'datafile', [ { @@ -63,10 +63,10 @@ const DLSDatafilesTable = ( ], !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('datafile'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('datafile'); const { data: totalDataCount } = useDatafileCount([ @@ -95,7 +95,7 @@ const DLSDatafilesTable = ( ); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx index a46306220..60b999f2c 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.test.tsx @@ -95,7 +95,7 @@ describe('DLS Dataset table component', () => { vi.mocked(useCart, { partial: true }).mockReturnValue({ data: [], - isLoading: false, + isPending: false, }); vi.mocked(useDatasetCount, { partial: true }).mockReturnValue({ data: 0, @@ -107,15 +107,15 @@ describe('DLS Dataset table component', () => { }); vi.mocked(useIds, { partial: true }).mockReturnValue({ data: [1], - isLoading: false, + isPending: false, }); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); axios.get = vi @@ -265,7 +265,7 @@ describe('DLS Dataset table component', () => { const addToCart = vi.fn(); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: addToCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -287,13 +287,13 @@ describe('DLS Dataset table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); const removeFromCart = vi.fn(); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: removeFromCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -323,7 +323,7 @@ describe('DLS Dataset table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); renderComponent(); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx index e9e6483d7..6237292b2 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsDatasetsTable.component.tsx @@ -52,7 +52,7 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { const dateFilter = useDateFilter(filters); const handleSort = useSort(); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'dataset', [ { @@ -64,10 +64,10 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { ], !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('dataset'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('dataset'); const { data: totalDataCount } = useDatasetCount([ @@ -100,7 +100,7 @@ const DLSDatasetsTable = (props: DLSDatasetsTableProps): React.ReactElement => { ); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx index 26f792e45..7c61380e2 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsMyDataTable.component.tsx @@ -92,7 +92,7 @@ const DLSMyDataTable = (): React.ReactElement => { const handleDefaultFilter = useReplaceFilter(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx index fd95ad570..d01d316e5 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.test.tsx @@ -93,11 +93,11 @@ describe('DLS Proposals table component', () => { vi.mocked(useInvestigationCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useInvestigationsInfinite, { partial: true }).mockReturnValue({ data: { pages: [rowData], pageParams: [] }, - isLoading: false, + isPending: false, }); }); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx index 2e7db7d0e..4ab585c41 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsProposalsTable.component.tsx @@ -69,7 +69,7 @@ const DLSProposalsTable = (): React.ReactElement => { const handleSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx index 2be60ff42..04dc5caa9 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.test.tsx @@ -8,12 +8,12 @@ import { import userEvent from '@testing-library/user-event'; import axios, { AxiosResponse } from 'axios'; import { - dGCommonInitialState, Investigation, + dGCommonInitialState, useInvestigationCount, useInvestigationsInfinite, } from 'datagateway-common'; -import { createMemoryHistory, History } from 'history'; +import { History, createMemoryHistory } from 'history'; import { Provider } from 'react-redux'; import { Router } from 'react-router-dom'; import configureStore from 'redux-mock-store'; @@ -96,11 +96,11 @@ describe('DLS Visits table component', () => { vi.mocked(useInvestigationCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useInvestigationsInfinite, { partial: true }).mockReturnValue({ data: { pages: [rowData], pageParams: [] }, - isLoading: false, + isPending: false, }); axios.get = vi @@ -328,7 +328,7 @@ describe('DLS Visits table component', () => { ], pageParams: [], }, - isLoading: false, + isPending: false, } ); @@ -340,7 +340,7 @@ describe('DLS Visits table component', () => { it('renders fine if no investigation instrument is returned', async () => { vi.mocked(useInvestigationCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useInvestigationsInfinite, { partial: true }).mockReturnValue({ data: { @@ -354,7 +354,7 @@ describe('DLS Visits table component', () => { ], pageParams: [], }, - isLoading: false, + isPending: false, }); renderComponent(); diff --git a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx index 4f903ab60..1f00fc4bf 100644 --- a/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/dls/dlsVisitsTable.component.tsx @@ -86,7 +86,7 @@ const DLSVisitsTable = (props: DLSVisitsTableProps): React.ReactElement => { const handleSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx b/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx index 25dea8368..214ceaf1d 100644 --- a/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/investigationTable.component.tsx @@ -51,15 +51,15 @@ const InvestigationTable = (): React.ReactElement => { }), }, ]); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'investigation', undefined, !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('investigation'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('investigation'); /* istanbul ignore next */ @@ -93,7 +93,7 @@ const InvestigationTable = (): React.ReactElement => { const handleSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx index 428e3d9ab..5cf1b64bd 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.test.tsx @@ -1,13 +1,22 @@ import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; -import type { StateType } from '../../../state/app.types'; -import { dGCommonInitialState, type DataPublication } from 'datagateway-common'; -import configureStore from 'redux-mock-store'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + render, + screen, + waitFor, + within, + type RenderResult, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios, { AxiosResponse } from 'axios'; +import { dGCommonInitialState, type DataPublication } from 'datagateway-common'; +import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; +import { Router, generatePath } from 'react-router-dom'; +import configureStore from 'redux-mock-store'; import thunk from 'redux-thunk'; -import { generatePath, Router } from 'react-router-dom'; -import { createMemoryHistory, type History } from 'history'; +import { paths } from '../../../page/pageContainer.component'; import { findAllRows, findCellInRow, @@ -15,17 +24,8 @@ import { findColumnIndexByName, findRowAt, } from '../../../setupTests'; -import { - render, - type RenderResult, - screen, - within, - waitFor, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import type { StateType } from '../../../state/app.types'; import ISISDataPublicationsTable from './isisDataPublicationsTable.component'; -import axios, { AxiosResponse } from 'axios'; -import { paths } from '../../../page/pageContainer.component'; describe('ISIS Data Publication table component', () => { const mockStore = configureStore([thunk]); @@ -116,7 +116,7 @@ describe('ISIS Data Publication table component', () => { case '/datapublications/count': return Promise.resolve({ data: 1, - isLoading: false, + isPending: false, }); default: return Promise.reject(`Endpoint not mocked: ${url}`); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx index 01a0bc5de..c6845ff05 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDataPublicationsTable.component.tsx @@ -151,7 +151,7 @@ const ISISDataPublicationsTable = ( const handleSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx index e8754c88e..874470074 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatafilesTable.component.tsx @@ -56,7 +56,7 @@ const ISISDatafilesTable = ( const dateFilter = useDateFilter(filters); const handleSort = useSort(); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'datafile', [ { @@ -66,10 +66,10 @@ const ISISDatafilesTable = ( ], !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('datafile'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('datafile'); const { data: totalDataCount } = useDatafileCount([ @@ -98,7 +98,7 @@ const ISISDatafilesTable = ( ); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx index 1d2bd47fc..cb8d136c3 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.test.tsx @@ -91,7 +91,7 @@ describe('ISIS Dataset table component', () => { vi.mocked(useCart, { partial: true }).mockReturnValue({ data: [], - isLoading: false, + isPending: false, }); vi.mocked(useDatasetCount, { partial: true }).mockReturnValue({ data: 0, @@ -102,15 +102,15 @@ describe('ISIS Dataset table component', () => { }); vi.mocked(useIds, { partial: true }).mockReturnValue({ data: [1], - isLoading: false, + isPending: false, }); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); axios.get = vi .fn() @@ -208,7 +208,7 @@ describe('ISIS Dataset table component', () => { const addToCart = vi.fn(); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: addToCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -230,13 +230,13 @@ describe('ISIS Dataset table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); const removeFromCart = vi.fn(); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: removeFromCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -266,7 +266,7 @@ describe('ISIS Dataset table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); renderComponent(); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx index 864d23844..09b0852fa 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisDatasetsTable.component.tsx @@ -57,7 +57,7 @@ const ISISDatasetsTable = ( const dateFilter = useDateFilter(filters); const handleSort = useSort(); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'dataset', [ { @@ -69,10 +69,10 @@ const ISISDatasetsTable = ( ], !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('dataset'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('dataset'); const { data: totalDataCount } = useDatasetCount([ @@ -105,7 +105,7 @@ const ISISDatasetsTable = ( ); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx index 8a8b5b3b9..056f98424 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.test.tsx @@ -1,32 +1,32 @@ -import ISISFacilityCyclesTable from './isisFacilityCyclesTable.component'; -import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; -import type { StateType } from '../../../state/app.types'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + render, + screen, + within, + type RenderResult, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { dGCommonInitialState, - type FacilityCycle, useFacilityCycleCount, useFacilityCyclesInfinite, + type FacilityCycle, } from 'datagateway-common'; -import configureStore from 'redux-mock-store'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createMemoryHistory, type History } from 'history'; import { Provider } from 'react-redux'; -import thunk from 'redux-thunk'; import { Router } from 'react-router-dom'; -import { createMemoryHistory, type History } from 'history'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; +import type { MockInstance } from 'vitest'; import { findAllRows, findCellInRow, findColumnHeaderByName, findColumnIndexByName, } from '../../../setupTests'; -import { - render, - type RenderResult, - screen, - within, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; -import type { MockInstance } from 'vitest'; +import type { StateType } from '../../../state/app.types'; +import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; +import ISISFacilityCyclesTable from './isisFacilityCyclesTable.component'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -83,7 +83,7 @@ describe('ISIS FacilityCycles table component', () => { vi.mocked(useFacilityCycleCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useFacilityCyclesInfinite, { partial: true }).mockReturnValue({ data: { pages: [rowData], pageParams: [] }, diff --git a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx index ec9090d14..a0066bb26 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisFacilityCyclesTable.component.tsx @@ -68,7 +68,7 @@ const ISISFacilityCyclesTable = ( const pushSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx index 6ff2169c6..4ed549e09 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.test.tsx @@ -1,32 +1,32 @@ -import ISISInstrumentsTable from './isisInstrumentsTable.component'; -import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; -import { StateType } from '../../../state/app.types'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + render, + screen, + within, + type RenderResult, +} from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import axios, { AxiosResponse } from 'axios'; import { dGCommonInitialState, Instrument, useInstrumentCount, useInstrumentsInfinite, } from 'datagateway-common'; -import configureStore from 'redux-mock-store'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { createMemoryHistory, History } from 'history'; import { Provider } from 'react-redux'; -import thunk from 'redux-thunk'; import { Router } from 'react-router-dom'; -import { createMemoryHistory, History } from 'history'; -import { - render, - type RenderResult, - screen, - within, -} from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; +import configureStore from 'redux-mock-store'; +import thunk from 'redux-thunk'; import { findAllRows, findCellInRow, findColumnHeaderByName, findColumnIndexByName, } from '../../../setupTests'; -import axios, { AxiosResponse } from 'axios'; +import { StateType } from '../../../state/app.types'; +import { initialState as dgDataViewInitialState } from '../../../state/reducers/dgdataview.reducer'; +import ISISInstrumentsTable from './isisInstrumentsTable.component'; vi.mock('datagateway-common', async () => { const originalModule = await vi.importActual('datagateway-common'); @@ -89,7 +89,7 @@ describe('ISIS Instruments table component', () => { vi.mocked(useInstrumentCount, { partial: true }).mockReturnValue({ data: 1, - isLoading: false, + isPending: false, }); vi.mocked(useInstrumentsInfinite, { partial: true }).mockReturnValue({ data: { pages: [rowData], pageParams: [] }, @@ -219,9 +219,7 @@ describe('ISIS Instruments table component', () => { renderComponent(); await user.click( - ( - await screen.findAllByRole('button', { name: 'Show details' }) - )[0] + (await screen.findAllByRole('button', { name: 'Show details' }))[0] ); expect( diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx index 27ac9abc8..e26fb51eb 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInstrumentsTable.component.tsx @@ -61,7 +61,7 @@ const ISISInstrumentsTable = ( const handleSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx index 1347445c6..51accfff3 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisInvestigationsTable.component.tsx @@ -110,15 +110,15 @@ const ISISInvestigationsTable = ( undefined, isMounted ); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'investigation', investigationQueryFilters, !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('investigation'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('investigation'); const selectedRows = React.useMemo( @@ -156,7 +156,7 @@ const ISISInvestigationsTable = ( ); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx index da615ad98..8ecf08d0a 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.test.tsx @@ -147,7 +147,7 @@ describe('ISIS MyData table component', () => { vi.mocked(useCart, { partial: true }).mockReturnValue({ data: [], - isLoading: false, + isPending: false, }); vi.mocked(useInvestigationCount, { partial: true }).mockReturnValue({ data: 0, @@ -158,15 +158,15 @@ describe('ISIS MyData table component', () => { }); vi.mocked(useIds, { partial: true }).mockReturnValue({ data: [1], - isLoading: false, + isPending: false, }); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: vi.fn(), - isLoading: false, + isPending: false, }); vi.mocked(readSciGatewayToken, { partial: true }).mockReturnValue({ username: 'testUser', @@ -380,7 +380,7 @@ describe('ISIS MyData table component', () => { const addToCart = vi.fn(); vi.mocked(useAddToCart, { partial: true }).mockReturnValue({ mutate: addToCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -402,13 +402,13 @@ describe('ISIS MyData table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); const removeFromCart = vi.fn(); vi.mocked(useRemoveFromCart, { partial: true }).mockReturnValue({ mutate: removeFromCart, - isLoading: false, + isPending: false, }); renderComponent(); @@ -437,7 +437,7 @@ describe('ISIS MyData table component', () => { parentEntities: [], }, ], - isLoading: false, + isPending: false, }); renderComponent(); diff --git a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx index 951560918..14ace809b 100644 --- a/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx +++ b/packages/datagateway-dataview/src/views/table/isis/isisMyDataTable.component.tsx @@ -91,7 +91,7 @@ const ISISMyDataTable = (): React.ReactElement => { undefined, isMounted ); - const { data: allIds, isInitialLoading: allIdsLoading } = useIds( + const { data: allIds, isPending: allIdsLoading } = useIds( 'investigation', [ { @@ -103,10 +103,10 @@ const ISISMyDataTable = (): React.ReactElement => { ], !disableSelectAll ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('investigation'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('investigation'); const selectedRows = React.useMemo( @@ -140,7 +140,7 @@ const ISISMyDataTable = (): React.ReactElement => { const handleSort = useSort(); const loadMoreRows = React.useCallback( - (offsetParams: IndexRange) => fetchNextPage({ pageParam: offsetParams }), + (_offsetParams: IndexRange) => fetchNextPage(), [fetchNextPage] ); diff --git a/packages/datagateway-download/package.json b/packages/datagateway-download/package.json index cb73b6f90..4b510e135 100644 --- a/packages/datagateway-download/package.json +++ b/packages/datagateway-download/package.json @@ -8,8 +8,8 @@ "@emotion/styled": "11.14.1", "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", - "@tanstack/react-query": "4.43.0", - "@tanstack/react-query-devtools": "4.43.0", + "@tanstack/react-query": "5.90.21", + "@tanstack/react-query-devtools": "5.91.3", "@types/jsrsasign": "10.5.2", "@types/lodash.chunk": "4.2.6", "@types/node": "24.12.0", @@ -48,6 +48,7 @@ }, "devDependencies": { "@dotenvx/dotenvx": "1.55.1", + "@tanstack/eslint-plugin-query": "5.91.4", "@testing-library/cypress": "10.1.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", diff --git a/packages/datagateway-download/src/App.tsx b/packages/datagateway-download/src/App.tsx index 1788314de..ad80e055f 100644 --- a/packages/datagateway-download/src/App.tsx +++ b/packages/datagateway-download/src/App.tsx @@ -1,20 +1,25 @@ +import { + QueryCache, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; import { DGThemeProvider, MicroFrontendId, Preloader, RequestPluginRerenderType, + queryCacheConfig, } from 'datagateway-common'; import React, { Component } from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { BrowserRouter as Router, Link, Route, Switch } from 'react-router-dom'; +import { Link, Route, BrowserRouter as Router, Switch } from 'react-router-dom'; import ConfigProvider, { DownloadSettingsContext } from './ConfigProvider'; import DOIGenerationForm from './DOIGenerationForm/DOIGenerationForm.component'; import AdminDownloadStatusTable from './downloadStatus/adminDownloadStatusTable.component'; -import DownloadTabs from './downloadTab/downloadTab.component'; import { QueryClientSettingsUpdater } from 'datagateway-common'; +import DownloadTabs from './downloadTab/downloadTab.component'; const queryClient = new QueryClient({ defaultOptions: { @@ -23,6 +28,7 @@ const queryClient = new QueryClient({ staleTime: 300000, }, }, + queryCache: new QueryCache(queryCacheConfig), }); export const QueryClientSettingsUpdaterContext: React.FC<{ diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx index dd872f452..d73521fc3 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.test.tsx @@ -59,12 +59,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); const renderComponent = ( diff --git a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx index 9d948110f..31806bb9d 100644 --- a/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx +++ b/packages/datagateway-download/src/DOIGenerationForm/DOIGenerationForm.component.tsx @@ -194,8 +194,8 @@ const DOIGenerationForm: React.FC = () => { draftMetadata={mintDraftData?.concept.attributes} onBackClick={handleBackClick} onConfirmClick={handleConfirmClick} - deleteLoading={deleteDraftStatus === 'loading'} - publishLoading={publishingStatus === 'loading'} + deleteLoading={deleteDraftStatus === 'pending'} + publishLoading={publishingStatus === 'pending'} /> ) : ( @@ -316,7 +316,7 @@ const DOIGenerationForm: React.FC = () => { disableMintButton={ typeof cart === 'undefined' || cart.length === 0 } - mintLoading={mintingDraftStatus === 'loading'} + mintLoading={mintingDraftStatus === 'pending'} onMintClick={handleMintClick} /> diff --git a/packages/datagateway-download/src/downloadApi.test.ts b/packages/datagateway-download/src/downloadApi.test.ts index f9721e851..a6486c768 100644 --- a/packages/datagateway-download/src/downloadApi.test.ts +++ b/packages/datagateway-download/src/downloadApi.test.ts @@ -1,333 +1,333 @@ -import axios from 'axios'; -import { handleICATError } from 'datagateway-common'; -import { - adminDownloadDeleted, - adminDownloadStatus, - downloadDeleted, - downloadPreparedCart, - fetchAdminDownloads, - fetchDownloads, - getDataUrl, - getPercentageComplete, -} from './downloadApi'; -import { mockedSettings } from './testData'; - -vi.mock('datagateway-common', async () => { - const originalModule = await vi.importActual('datagateway-common'); - - return { - __esModule: true, - ...originalModule, - handleICATError: vi.fn(), - }; -}); - -describe('Download Cart API functions test', () => { - afterEach(() => { - vi.mocked(handleICATError).mockClear(); - }); - - describe('downloadPreparedCart', () => { - it('opens a link to download "test-file" upon successful response for a download request', async () => { - vi.spyOn(document, 'createElement'); - vi.spyOn(document.body, 'appendChild'); - - await downloadPreparedCart('test-id', 'test-file.zip', { - idsUrl: mockedSettings.idsUrl, - }); - - expect(document.createElement).toHaveBeenCalledWith('a'); - - // Create our prepared cart download link. - const link = document.createElement('a'); - link.href = `${ - mockedSettings.idsUrl - }/getData?sessionId=${null}&preparedId=${'test-id'}&outname=${'test-file.zip'}`; - link.style.display = 'none'; - link.target = '_blank'; - - expect(document.body.appendChild).toHaveBeenCalledWith(link); - }); - }); -}); - -describe('Download Status API functions test', () => { - describe('fetchDownloads', () => { - const downloadsMockData = [ - { - createdAt: '2020-01-01T01:01:01Z', - downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], - email: 'test@email.com', - facilityName: mockedSettings.facilityName, - fileName: 'test-file-1', - fullName: 'Person 1', - id: 1, - isDeleted: false, - isEmailSent: true, - isTwoLevel: false, - preparedId: 'e44acee7-2211-4aae-bffb-f6c0e417f43d', - sessionId: '6bf8e6e4-58a9-11ea-b823-005056893dd9', - size: 0, - status: 'COMPLETE', - transport: 'https', - userName: 'test user', - }, - ]; - - it('returns downloads upon successful response', async () => { - axios.get = vi.fn().mockImplementation(() => - Promise.resolve({ - data: downloadsMockData, - }) - ); - - const returnData = await fetchDownloads({ - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(downloadsMockData); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/downloads`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - queryOffset: 'where download.isDeleted = false', - }, - } - ); - }); - - it('returns downloads with a custom queryOffset upon successful response', async () => { - const downloadsData = { - ...downloadsMockData[0], - isDeleted: true, - }; - - axios.get = vi.fn().mockImplementation(() => - Promise.resolve({ - data: downloadsData, - }) - ); - - const returnData = await fetchDownloads( - { - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }, - 'where download.isDeleted = true' - ); - - expect(returnData).toBe(downloadsData); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/downloads`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - queryOffset: 'where download.isDeleted = true', - }, - } - ); - }); - }); - - describe('downloadDeleted', () => { - it('successfully sets a download as deleted', async () => { - axios.put = vi.fn().mockImplementation(() => Promise.resolve()); - - await downloadDeleted(1, true, { - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - const params = new URLSearchParams(); - params.append('facilityName', mockedSettings.facilityName); - params.append('sessionId', ''); - params.append('value', 'true'); - - expect(axios.put).toHaveBeenCalled(); - expect(axios.put).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/download/1/isDeleted`, - params - ); - }); - }); - - describe('getDataUrl', () => { - it('successfully constructs a download link with given parameters', () => { - const preparedId = 'test-prepared-id'; - const fileName = 'test-filename'; - const idsUrl = 'test-ids-url'; - - const result = getDataUrl(preparedId, fileName, idsUrl); - [preparedId, fileName, idsUrl].forEach((entry) => { - expect(result).toContain(entry); - }); - }); - }); -}); - -describe('Admin Download Status API functions test', () => { - describe('fetchAdminDownloads', () => { - const downloadsMockData = [ - { - createdAt: '2020-01-01T01:01:01Z', - downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], - email: 'test@email.com', - facilityName: mockedSettings.facilityName, - fileName: 'test-file-1', - fullName: 'Person 1', - id: 1, - isDeleted: false, - isEmailSent: true, - isTwoLevel: false, - preparedId: 'e44acee7-2211-4aae-bffb-f6c0e417f43d', - sessionId: '6bf8e6e4-58a9-11ea-b823-005056893dd9', - size: 0, - status: 'COMPLETE', - transport: 'https', - userName: 'test user', - }, - ]; - - it('returns downloads upon successful response', async () => { - axios.get = vi.fn().mockImplementation(() => - Promise.resolve({ - data: downloadsMockData, - }) - ); - - const returnData = await fetchAdminDownloads({ - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - expect(returnData).toBe(downloadsMockData); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/admin/downloads`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - queryOffset: 'where download.isDeleted = false', - }, - } - ); - }); - - it('returns downloads with a custom queryOffset upon successful response', async () => { - const downloadsData = { - ...downloadsMockData[0], - isDeleted: true, - }; - - axios.get = vi.fn().mockImplementation(() => - Promise.resolve({ - data: downloadsData, - }) - ); - - const returnData = await fetchAdminDownloads( - { - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }, - 'where download.isDeleted = true' - ); - - expect(returnData).toBe(downloadsData); - expect(axios.get).toHaveBeenCalled(); - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/admin/downloads`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - queryOffset: 'where download.isDeleted = true', - }, - } - ); - }); - }); - - describe('adminDownloadDeleted', () => { - it('successfully sets a download as deleted', async () => { - axios.put = vi.fn().mockImplementation(() => Promise.resolve()); - - await adminDownloadDeleted(1, true, { - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - const params = new URLSearchParams(); - params.append('facilityName', mockedSettings.facilityName); - params.append('sessionId', ''); - params.append('value', 'true'); - - expect(axios.put).toHaveBeenCalled(); - expect(axios.put).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/admin/download/1/isDeleted`, - params - ); - }); - }); - - describe('adminDownloadStatus', () => { - it('successfully sets the status of a download', async () => { - axios.put = vi.fn().mockImplementation(() => Promise.resolve()); - - await adminDownloadStatus(1, 'RESTORING', { - facilityName: mockedSettings.facilityName, - downloadApiUrl: mockedSettings.downloadApiUrl, - }); - - const params = new URLSearchParams(); - params.append('facilityName', mockedSettings.facilityName); - params.append('sessionId', ''); - params.append('value', 'RESTORING'); - - expect(axios.put).toHaveBeenCalled(); - expect(axios.put).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/admin/download/1/status`, - params - ); - }); - }); - - describe('getPercentageComplete', () => { - it('should return the percentage of a download restore from 0 to 100', async () => { - axios.get = vi.fn().mockResolvedValue({ - data: '2', - }); - - const response = await getPercentageComplete({ - preparedId: '1', - settings: { - idsUrl: mockedSettings.idsUrl, - }, - }); - - expect(response).toEqual(2); - }); - - it('should return the status of a download restore', async () => { - axios.get = vi.fn().mockResolvedValue({ - data: 'INVALID', - }); - - const response = await getPercentageComplete({ - preparedId: '1', - settings: { - idsUrl: mockedSettings.idsUrl, - }, - }); - - expect(response).toEqual('INVALID'); - }); - }); -}); +import axios from 'axios'; +import { handleICATError } from 'datagateway-common'; +import { + adminDownloadDeleted, + adminDownloadStatus, + downloadDeleted, + downloadPreparedCart, + fetchAdminDownloads, + fetchDownloads, + getDataUrl, + getPercentageComplete, +} from './downloadApi'; +import { mockedSettings } from './testData'; + +vi.mock('datagateway-common', async () => { + const originalModule = await vi.importActual('datagateway-common'); + + return { + __esModule: true, + ...originalModule, + handleICATError: vi.fn(), + }; +}); + +describe('Download Cart API functions test', () => { + afterEach(() => { + vi.mocked(handleICATError).mockClear(); + }); + + describe('downloadPreparedCart', () => { + it('opens a link to download "test-file" upon successful response for a download request', async () => { + vi.spyOn(document, 'createElement'); + vi.spyOn(document.body, 'appendChild'); + + await downloadPreparedCart('test-id', 'test-file.zip', { + idsUrl: mockedSettings.idsUrl, + }); + + expect(document.createElement).toHaveBeenCalledWith('a'); + + // Create our prepared cart download link. + const link = document.createElement('a'); + link.href = `${ + mockedSettings.idsUrl + }/getData?sessionId=${null}&preparedId=${'test-id'}&outname=${'test-file.zip'}`; + link.style.display = 'none'; + link.target = '_blank'; + + expect(document.body.appendChild).toHaveBeenCalledWith(link); + }); + }); +}); + +describe('Download Status API functions test', () => { + describe('fetchDownloads', () => { + const downloadsMockData = [ + { + createdAt: '2020-01-01T01:01:01Z', + downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], + email: 'test@email.com', + facilityName: mockedSettings.facilityName, + fileName: 'test-file-1', + fullName: 'Person 1', + id: 1, + isDeleted: false, + isEmailSent: true, + isTwoLevel: false, + preparedId: 'e44acee7-2211-4aae-bffb-f6c0e417f43d', + sessionId: '6bf8e6e4-58a9-11ea-b823-005056893dd9', + size: 0, + status: 'COMPLETE', + transport: 'https', + userName: 'test user', + }, + ]; + + it('returns downloads upon successful response', async () => { + axios.get = vi.fn().mockImplementation(() => + Promise.resolve({ + data: downloadsMockData, + }) + ); + + const returnData = await fetchDownloads({ + facilityName: mockedSettings.facilityName, + downloadApiUrl: mockedSettings.downloadApiUrl, + }); + + expect(returnData).toBe(downloadsMockData); + expect(axios.get).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/downloads`, + { + params: { + sessionId: null, + facilityName: mockedSettings.facilityName, + queryOffset: 'where download.isDeleted = false', + }, + } + ); + }); + + it('returns downloads with a custom queryOffset upon successful response', async () => { + const downloadsData = { + ...downloadsMockData[0], + isDeleted: true, + }; + + axios.get = vi.fn().mockImplementation(() => + Promise.resolve({ + data: downloadsData, + }) + ); + + const returnData = await fetchDownloads( + { + facilityName: mockedSettings.facilityName, + downloadApiUrl: mockedSettings.downloadApiUrl, + }, + 'where download.isDeleted = true' + ); + + expect(returnData).toBe(downloadsData); + expect(axios.get).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/downloads`, + { + params: { + sessionId: null, + facilityName: mockedSettings.facilityName, + queryOffset: 'where download.isDeleted = true', + }, + } + ); + }); + }); + + describe('downloadDeleted', () => { + it('successfully sets a download as deleted', async () => { + axios.put = vi.fn().mockImplementation(() => Promise.resolve()); + + await downloadDeleted(1, true, { + facilityName: mockedSettings.facilityName, + downloadApiUrl: mockedSettings.downloadApiUrl, + }); + + const params = new URLSearchParams(); + params.append('facilityName', mockedSettings.facilityName); + params.append('sessionId', ''); + params.append('value', 'true'); + + expect(axios.put).toHaveBeenCalled(); + expect(axios.put).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/user/download/1/isDeleted`, + params + ); + }); + }); + + describe('getDataUrl', () => { + it('successfully constructs a download link with given parameters', () => { + const preparedId = 'test-prepared-id'; + const fileName = 'test-filename'; + const idsUrl = 'test-ids-url'; + + const result = getDataUrl(preparedId, fileName, idsUrl); + [preparedId, fileName, idsUrl].forEach((entry) => { + expect(result).toContain(entry); + }); + }); + }); +}); + +describe('Admin Download Status API functions test', () => { + describe('fetchAdminDownloads', () => { + const downloadsMockData = [ + { + createdAt: '2020-01-01T01:01:01Z', + downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], + email: 'test@email.com', + facilityName: mockedSettings.facilityName, + fileName: 'test-file-1', + fullName: 'Person 1', + id: 1, + isDeleted: false, + isEmailSent: true, + isTwoLevel: false, + preparedId: 'e44acee7-2211-4aae-bffb-f6c0e417f43d', + sessionId: '6bf8e6e4-58a9-11ea-b823-005056893dd9', + size: 0, + status: 'COMPLETE', + transport: 'https', + userName: 'test user', + }, + ]; + + it('returns downloads upon successful response', async () => { + axios.get = vi.fn().mockImplementation(() => + Promise.resolve({ + data: downloadsMockData, + }) + ); + + const returnData = await fetchAdminDownloads({ + facilityName: mockedSettings.facilityName, + downloadApiUrl: mockedSettings.downloadApiUrl, + }); + + expect(returnData).toBe(downloadsMockData); + expect(axios.get).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/admin/downloads`, + { + params: { + sessionId: null, + facilityName: mockedSettings.facilityName, + queryOffset: 'where download.isDeleted = false', + }, + } + ); + }); + + it('returns downloads with a custom queryOffset upon successful response', async () => { + const downloadsData = { + ...downloadsMockData[0], + isDeleted: true, + }; + + axios.get = vi.fn().mockImplementation(() => + Promise.resolve({ + data: downloadsData, + }) + ); + + const returnData = await fetchAdminDownloads( + { + facilityName: mockedSettings.facilityName, + downloadApiUrl: mockedSettings.downloadApiUrl, + }, + 'where download.isDeleted = true' + ); + + expect(returnData).toBe(downloadsData); + expect(axios.get).toHaveBeenCalled(); + expect(axios.get).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/admin/downloads`, + { + params: { + sessionId: null, + facilityName: mockedSettings.facilityName, + queryOffset: 'where download.isDeleted = true', + }, + } + ); + }); + }); + + describe('adminDownloadDeleted', () => { + it('successfully sets a download as deleted', async () => { + axios.put = vi.fn().mockImplementation(() => Promise.resolve()); + + await adminDownloadDeleted(1, true, { + facilityName: mockedSettings.facilityName, + downloadApiUrl: mockedSettings.downloadApiUrl, + }); + + const params = new URLSearchParams(); + params.append('facilityName', mockedSettings.facilityName); + params.append('sessionId', ''); + params.append('value', 'true'); + + expect(axios.put).toHaveBeenCalled(); + expect(axios.put).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/admin/download/1/isDeleted`, + params + ); + }); + }); + + describe('adminDownloadStatus', () => { + it('successfully sets the status of a download', async () => { + axios.put = vi.fn().mockImplementation(() => Promise.resolve()); + + await adminDownloadStatus(1, 'RESTORING', { + facilityName: mockedSettings.facilityName, + downloadApiUrl: mockedSettings.downloadApiUrl, + }); + + const params = new URLSearchParams(); + params.append('facilityName', mockedSettings.facilityName); + params.append('sessionId', ''); + params.append('value', 'RESTORING'); + + expect(axios.put).toHaveBeenCalled(); + expect(axios.put).toHaveBeenCalledWith( + `${mockedSettings.downloadApiUrl}/admin/download/1/status`, + params + ); + }); + }); + + describe('getPercentageComplete', () => { + it('should return the percentage of a download restore from 0 to 100', async () => { + axios.get = vi.fn().mockResolvedValue({ + data: '2', + }); + + const response = await getPercentageComplete({ + preparedId: '1', + settings: { + idsUrl: mockedSettings.idsUrl, + }, + }); + + expect(response).toEqual(2); + }); + + it('should return the status of a download restore', async () => { + axios.get = vi.fn().mockResolvedValue({ + data: 'INVALID', + }); + + const response = await getPercentageComplete({ + preparedId: '1', + settings: { + idsUrl: mockedSettings.idsUrl, + }, + }); + + expect(response).toEqual('INVALID'); + }); + }); +}); diff --git a/packages/datagateway-download/src/downloadApiHooks.test.tsx b/packages/datagateway-download/src/downloadApiHooks.test.tsx index c14b0f4d1..8bbcd9298 100644 --- a/packages/datagateway-download/src/downloadApiHooks.test.tsx +++ b/packages/datagateway-download/src/downloadApiHooks.test.tsx @@ -1,11 +1,15 @@ -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { + QueryCache, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; import { act, renderHook, waitFor } from '@testing-library/react'; import axios, { AxiosError } from 'axios'; +import * as dgCommon from 'datagateway-common'; import { ContributorType, Download, - handleDOIAPIError, - handleICATError, + queryCacheConfig, } from 'datagateway-common'; import { createMemoryHistory } from 'history'; import * as React from 'react'; @@ -30,18 +34,6 @@ import { } from './downloadApiHooks'; import { mockCartItems, mockDownloadItems, mockedSettings } from './testData'; -vi.mock('datagateway-common', async () => { - const originalModule = await vi.importActual('datagateway-common'); - - return { - __esModule: true, - ...originalModule, - handleICATError: vi.fn(), - retryICATErrors: vi.fn().mockReturnValue(false), - handleDOIAPIError: vi.fn(), - }; -}); - const createTestQueryClient = (): QueryClient => new QueryClient({ defaultOptions: { @@ -51,12 +43,7 @@ const createTestQueryClient = (): QueryClient => retryDelay: 0, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, + queryCache: new QueryCache(queryCacheConfig), }); const createReactQueryWrapper = ( @@ -90,6 +77,9 @@ describe('Download API react-query hooks test', () => { detail: { type: string; payload?: unknown }; }>[] = []; + const handleICATErrorSpy = vi.spyOn(dgCommon, 'handleICATError'); + const handleDOIAPIErrorSpy = vi.spyOn(dgCommon, 'handleDOIAPIError'); + beforeEach(() => { events = []; @@ -152,22 +142,6 @@ describe('Download API react-query hooks test', () => { ); expect(result.current.data).toEqual(downloadCartMockData.cartItems); }); - - it('sends axios request to fetch cart and calls handleICATError on failure', async () => { - axios.get = vi.fn().mockRejectedValue({ - message: 'Test error message', - }); - - const { result } = renderHook(() => useCart(), { - wrapper: createReactQueryWrapper(), - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); - }); }); describe('useRemoveAllFromCart', () => { @@ -233,8 +207,8 @@ describe('Download API react-query hooks test', () => { { params: { sessionId: null, items: '*' } } ); expect(result.current.failureCount).toBe(2); - expect(handleICATError).toHaveBeenCalledTimes(1); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledTimes(1); + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'Test error message', }); }); @@ -303,8 +277,8 @@ describe('Download API react-query hooks test', () => { { params: { sessionId: null, items: 'investigation 1' } } ); expect(result.current.failureCount).toBe(2); - expect(handleICATError).toHaveBeenCalledTimes(1); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledTimes(1); + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'Test error message', }); }); @@ -329,27 +303,6 @@ describe('Download API react-query hooks test', () => { ); expect(result.current.data).toEqual(true); }); - - it('returns false in the event of an error and logs error upon unsuccessful response', async () => { - axios.get = vi.fn().mockImplementation(() => - Promise.reject({ - message: 'Test error message', - }) - ); - - const { result } = renderHook(() => useIsTwoLevel(), { - wrapper: createReactQueryWrapper(), - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.idsUrl}/isTwoLevel` - ); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); - }); }); describe('useFileCountsAndSizes', () => { @@ -419,12 +372,6 @@ describe('Download API react-query hooks test', () => { }, } ); - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'simulating a failed response', - }, - false - ); }); }); @@ -450,32 +397,6 @@ describe('Download API react-query hooks test', () => { ); expect(result.current.data).toEqual(mockDownloadItems); }); - - it('should call handleICATError on failure', async () => { - axios.get = vi.fn().mockRejectedValue({ - message: 'Test error message', - }); - - const { result } = renderHook(() => useDownloads(), { - wrapper: createReactQueryWrapper(), - }); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/user/downloads`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - queryOffset: 'where download.isDeleted = false', - }, - } - ); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); - }); }); describe('useDownloadOrRestoreDownload', () => { @@ -597,7 +518,7 @@ describe('Download API react-query hooks test', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'Test error message', }); }); @@ -610,7 +531,7 @@ describe('Download API react-query hooks test', () => { // first, test fetching initial data const { result } = renderHook( - () => useAdminDownloads({ initialQueryOffset: 'LIMIT 0, 50' }), + () => useAdminDownloads({ filters: {}, sort: {} }), { wrapper: createReactQueryWrapper(), } @@ -625,7 +546,7 @@ describe('Download API react-query hooks test', () => { params: { sessionId: null, facilityName: mockedSettings.facilityName, - queryOffset: 'LIMIT 0, 50', + queryOffset: `WHERE download.facilityName = '${mockedSettings.facilityName}' ORDER BY download.id ASC LIMIT 0, 50`, }, } ); @@ -633,9 +554,7 @@ describe('Download API react-query hooks test', () => { // then test fetching next page - await result.current.fetchNextPage({ - pageParam: 'LIMIT 50, 100', - }); + await result.current.fetchNextPage(); await waitFor(() => expect( !result.current.isFetchingNextPage && result.current.isSuccess @@ -649,7 +568,7 @@ describe('Download API react-query hooks test', () => { params: { sessionId: null, facilityName: mockedSettings.facilityName, - queryOffset: 'LIMIT 50, 100', + queryOffset: `WHERE download.facilityName = '${mockedSettings.facilityName}' ORDER BY download.id ASC LIMIT 50, 25`, }, } ); @@ -658,35 +577,6 @@ describe('Download API react-query hooks test', () => { mockDownloadItems, ]); }); - - it('should call handleICATError when an error is encountered', async () => { - axios.get = vi.fn().mockRejectedValue({ - message: 'Test error message', - }); - - const { result } = renderHook( - () => useAdminDownloads({ initialQueryOffset: 'LIMIT 0, 50' }), - { - wrapper: createReactQueryWrapper(), - } - ); - - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(axios.get).toHaveBeenCalledWith( - `${mockedSettings.downloadApiUrl}/admin/downloads`, - { - params: { - sessionId: null, - facilityName: mockedSettings.facilityName, - queryOffset: 'LIMIT 0, 50', - }, - } - ); - expect(handleICATError).toHaveBeenCalledWith({ - message: 'Test error message', - }); - }); }); describe('useAdminDownloadDeleted', () => { @@ -714,7 +604,8 @@ describe('Download API react-query hooks test', () => { // fetchAdminDownloads from useAdminDownloads if ( url === `${mockedSettings.downloadApiUrl}/admin/downloads` && - params.queryOffset === 'LIMIT 0, 50' + params.queryOffset === + `WHERE download.facilityName = '${mockedSettings.facilityName}' ORDER BY download.id ASC LIMIT 0, 50` ) return Promise.resolve({ data: isMutated @@ -742,9 +633,7 @@ describe('Download API react-query hooks test', () => { const { result } = renderHook( () => ({ - useAdminDownloads: useAdminDownloads({ - initialQueryOffset: 'LIMIT 0, 50', - }), + useAdminDownloads: useAdminDownloads({ filters: {}, sort: {} }), useAdminDownloadDeleted: useAdminDownloadDeleted(), }), { @@ -819,7 +708,8 @@ describe('Download API react-query hooks test', () => { // fetchAdminDownloads from useAdminDownloads if ( url === `${mockedSettings.downloadApiUrl}/admin/downloads` && - params.queryOffset === 'LIMIT 0, 50' + params.queryOffset === + `WHERE download.facilityName = '${mockedSettings.facilityName}' ORDER BY download.id ASC LIMIT 0, 50` ) return Promise.resolve({ data: isMutated @@ -847,9 +737,7 @@ describe('Download API react-query hooks test', () => { const { result } = renderHook( () => ({ - useAdminDownloads: useAdminDownloads({ - initialQueryOffset: 'LIMIT 0, 50', - }), + useAdminDownloads: useAdminDownloads({ filters: {}, sort: {} }), useAdminDownloadDeleted: useAdminDownloadDeleted(), }), { @@ -893,7 +781,7 @@ describe('Download API react-query hooks test', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'Test error message', }); }); @@ -934,9 +822,7 @@ describe('Download API react-query hooks test', () => { const { result } = renderHook( () => ({ - useAdminDownloads: useAdminDownloads({ - initialQueryOffset: 'LIMIT 0, 50 ', - }), + useAdminDownloads: useAdminDownloads({ filters: {}, sort: {} }), useAdminUpdateDownloadStatus: useAdminUpdateDownloadStatus(), }), { @@ -971,9 +857,7 @@ describe('Download API react-query hooks test', () => { const { result } = renderHook( () => ({ - useAdminDownloads: useAdminDownloads({ - initialQueryOffset: 'LIMIT 0, 50', - }), + useAdminDownloads: useAdminDownloads({ filters: {}, sort: {} }), useAdminUpdateDownloadStatus: useAdminUpdateDownloadStatus(), }), { wrapper: createReactQueryWrapper() } @@ -982,15 +866,14 @@ describe('Download API react-query hooks test', () => { await waitFor(() => expect(result.current.useAdminDownloads.isSuccess).toBe(true) ); - result.current.useAdminUpdateDownloadStatus.mutate({ - downloadId: 1, - status: 'PREPARING', - }); - await waitFor(() => - expect(result.current.useAdminUpdateDownloadStatus.isError).toBe(true) - ); + await expect( + result.current.useAdminUpdateDownloadStatus.mutateAsync({ + downloadId: 1, + status: 'PREPARING', + }) + ).rejects.toThrowError(); - expect(handleICATError).toHaveBeenCalledWith({ + expect(handleICATErrorSpy).toHaveBeenCalledWith({ message: 'Test error message', }); expect(result.current.useAdminDownloads.data?.pages).toEqual([ @@ -1039,31 +922,6 @@ describe('Download API react-query hooks test', () => { expect(result.current.data).toEqual('UNKNOWN'); }); - - it('should call handleICATError when an error is encountered', async () => { - axios.get = vi.fn().mockRejectedValue({ - message: 'Test error message', - }); - - const { result } = renderHook( - () => - useDownloadPercentageComplete({ - download: mockDownloadItems[0], - idsUrl: 'https://example.com/ids', - }), - { - wrapper: createReactQueryWrapper(), - } - ); - await waitFor(() => expect(result.current.isError).toBe(true)); - - expect(handleICATError).toHaveBeenCalledWith( - { - message: 'Test error message', - }, - false - ); - }); }); describe('useMintDraftCart', () => { @@ -1136,13 +994,7 @@ describe('Download API react-query hooks test', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleDOIAPIError).toHaveBeenCalledWith( - error, - undefined, - undefined, - true, - true - ); + expect(handleDOIAPIErrorSpy).toHaveBeenCalledWith(error, true, true); expect(axios.post).toHaveBeenCalledWith( `${mockedSettings.doiMinterUrl}/draft`, { @@ -1217,7 +1069,7 @@ describe('Download API react-query hooks test', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleDOIAPIError).toHaveBeenCalledWith(error, '1', undefined); + expect(handleDOIAPIErrorSpy).toHaveBeenCalledWith(error, true, true); expect(axios.put).toHaveBeenCalledWith( `${mockedSettings.doiMinterUrl}/draft/1/publish`, undefined, @@ -1264,13 +1116,7 @@ describe('Download API react-query hooks test', () => { }); await waitFor(() => expect(result.current.isError).toBe(true)); - expect(handleDOIAPIError).toHaveBeenCalledWith( - error, - undefined, - undefined, - true, - true - ); + expect(handleDOIAPIErrorSpy).toHaveBeenCalledWith(error, true, true); expect(axios.delete).toHaveBeenCalledWith( `${mockedSettings.doiMinterUrl}/draft/1`, { headers: { Authorization: 'Bearer null' } } diff --git a/packages/datagateway-download/src/downloadApiHooks.ts b/packages/datagateway-download/src/downloadApiHooks.ts index 2251e294f..7d49c71c2 100644 --- a/packages/datagateway-download/src/downloadApiHooks.ts +++ b/packages/datagateway-download/src/downloadApiHooks.ts @@ -1,24 +1,20 @@ import { InfiniteData, - UseInfiniteQueryResult, - UseMutationResult, - UseQueryOptions, - UseQueryResult, + queryOptions, useInfiniteQuery, useMutation, useQueries, useQuery, - useQueryClient, } from '@tanstack/react-query'; import { AxiosError } from 'axios'; import { - DOIDraftResponse, DOIMetadata, - DOIResponse, Download, DownloadCartItem, DownloadStatus, - User, + FiltersType, + INFINITE_SCROLL_BATCH_SIZE, + SortType, fetchDownloadCart, getDownload, handleDOIAPIError, @@ -29,8 +25,6 @@ import pLimit from 'p-limit'; import React from 'react'; import { DownloadSettingsContext } from './ConfigProvider'; import { - DownloadProgress, - FileSizeAndCount, adminDownloadDeleted, adminDownloadStatus, deleteDraftDOI, @@ -77,106 +71,95 @@ export enum QueryKeys { CART = 'cart', } -/** - * Defines the function that when called will roll back any optimistic changes - * performed during a mutation. - */ -type RollbackFunction = () => void; - -export const useCart = (): UseQueryResult => { +export const useCart = () => { const settings = React.useContext(DownloadSettingsContext); const { facilityName, downloadApiUrl } = settings; const retryICATErrors = useRetryICATErrors(); - return useQuery( - [QueryKeys.CART], - () => + return useQuery({ + queryKey: [QueryKeys.CART, facilityName, downloadApiUrl], + queryFn: () => fetchDownloadCart({ facilityName, downloadApiUrl, }), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - staleTime: 0, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + staleTime: 0, + }); }; -export const useRemoveAllFromCart = (): UseMutationResult< - void, - AxiosError, - void -> => { - const queryClient = useQueryClient(); +export const useRemoveAllFromCart = () => { const settings = React.useContext(DownloadSettingsContext); const { facilityName, downloadApiUrl } = settings; - return useMutation( - () => removeAllDownloadCartItems({ facilityName, downloadApiUrl }), - { - onSuccess: () => { - queryClient.setQueryData([QueryKeys.CART], []); - }, - retry: (failureCount, error) => { - // if we get 431 we know this is an intermittent error so retry - if (error.response?.status === 431 && failureCount < 3) { - return true; - } else { - return false; - } - }, - onError: (error) => { - handleICATError(error); - }, - } - ); + return useMutation({ + mutationFn: () => + removeAllDownloadCartItems({ facilityName, downloadApiUrl }), + + onSuccess: (_data, _variables, _onMutateResult, context) => { + context.client.setQueriesData({ queryKey: [QueryKeys.CART] }, []); + }, + + retry: (failureCount: number, error: AxiosError) => { + // if we get 431 we know this is an intermittent error so retry + if (error.response?.status === 431 && failureCount < 3) { + return true; + } else { + return false; + } + }, + + onError: (error) => { + handleICATError(error); + }, + }); }; -export const useRemoveEntityFromCart = (): UseMutationResult< - DownloadCartItem[], - AxiosError, - { entityId: number; entityType: 'investigation' | 'dataset' | 'datafile' } -> => { - const queryClient = useQueryClient(); +export const useRemoveEntityFromCart = () => { const settings = React.useContext(DownloadSettingsContext); const { facilityName, downloadApiUrl } = settings; - return useMutation( - ({ entityId, entityType }) => + return useMutation({ + mutationFn: ({ + entityId, + entityType, + }: { + entityId: number; + entityType: 'investigation' | 'dataset' | 'datafile'; + }) => removeFromCart(entityType, [entityId], { facilityName, downloadApiUrl, }), - { - onSuccess: (data) => { - queryClient.setQueryData([QueryKeys.CART], data); - }, - retry: (failureCount, error) => { - // if we get 431 we know this is an intermittent error so retry - if (error.response?.status === 431 && failureCount < 3) { - return true; - } else { - return false; - } - }, - onError: (error) => { - handleICATError(error); - }, - } - ); + + onSuccess: (data, _variables, _onMutateResult, context) => { + context.client.setQueriesData({ queryKey: [QueryKeys.CART] }, data); + }, + + retry: (failureCount: number, error: AxiosError) => { + // if we get 431 we know this is an intermittent error so retry + if (error.response?.status === 431 && failureCount < 3) { + return true; + } else { + return false; + } + }, + + onError: (error) => { + handleICATError(error); + }, + }); }; -export const useIsTwoLevel = (): UseQueryResult => { +export const useIsTwoLevel = () => { const settings = React.useContext(DownloadSettingsContext); const { idsUrl } = settings; const retryICATErrors = useRetryICATErrors(); - return useQuery(['isTwoLevel'], () => getIsTwoLevel({ idsUrl }), { - onError: (error) => { - handleICATError(error); - }, + return useQuery({ + queryKey: ['isTwoLevel', idsUrl], + queryFn: () => getIsTwoLevel({ idsUrl }), + meta: { icatError: true }, retry: retryICATErrors, staleTime: Infinity, }); @@ -184,9 +167,7 @@ export const useIsTwoLevel = (): UseQueryResult => { const fileSizeAndCountLimit = pLimit(20); -export const useFileSizesAndCounts = ( - data: DownloadCartItem[] | undefined -): UseQueryResult[] => { +export const useFileSizesAndCounts = (data: DownloadCartItem[] | undefined) => { const settings = React.useContext(DownloadSettingsContext); const { apiUrl } = settings; const retryICATErrors = useRetryICATErrors(); @@ -195,18 +176,16 @@ export const useFileSizesAndCounts = ( return data ? data.map((cartItem) => { const { entityId, entityType } = cartItem; - return { - queryKey: ['fileSizeAndCount', entityId], + return queryOptions({ + queryKey: ['fileSizeAndCount', entityId, apiUrl], queryFn: () => fileSizeAndCountLimit(getFileSizeAndCount, entityId, entityType, { apiUrl, }), - onError: (error: AxiosError) => { - handleICATError(error, false); - }, + meta: { icatError: true }, retry: retryICATErrors, staleTime: Infinity, - } as UseQueryOptions; + }); }) : []; }, [data, retryICATErrors, apiUrl]); @@ -219,33 +198,28 @@ export const useFileSizesAndCounts = ( /** * A React hook that fetches all downloads created by the user. */ -export const useDownloads = ( - queryOptions?: UseQueryOptions< - Download[], - AxiosError, - TData, - [QueryKeys.DOWNLOADS] - > -): UseQueryResult => { +export const useDownloads = ( + selectFn?: (data: Download[]) => TSelectData +) => { // Load the download settings for use. const downloadSettings = React.useContext(DownloadSettingsContext); const retryICATErrors = useRetryICATErrors(); - return useQuery( - [QueryKeys.DOWNLOADS], - () => + return useQuery({ + queryKey: [ + QueryKeys.DOWNLOADS, + downloadSettings.facilityName, + downloadSettings.downloadApiUrl, + ], + queryFn: () => fetchDownloads({ facilityName: downloadSettings.facilityName, downloadApiUrl: downloadSettings.downloadApiUrl, }), - { - onError: (error) => { - handleICATError(error); - }, - retry: retryICATErrors, - ...queryOptions, - } - ); + meta: { icatError: true }, + retry: retryICATErrors, + select: selectFn, + }); }; export interface UseDownloadDeletedParams { @@ -256,68 +230,131 @@ export interface UseDownloadDeletedParams { /** * A React query that provides a mutation for deleting a download item. */ -export const useDownloadOrRestoreDownload = (): UseMutationResult< - void, - AxiosError, - UseDownloadDeletedParams, - RollbackFunction -> => { - const queryClient = useQueryClient(); +export const useDownloadOrRestoreDownload = () => { // Load the download settings for use. const downloadSettings = React.useContext(DownloadSettingsContext); - return useMutation( - ({ downloadId, deleted }) => + return useMutation({ + mutationFn: ({ downloadId, deleted }: UseDownloadDeletedParams) => downloadDeleted(downloadId, deleted, { facilityName: downloadSettings.facilityName, downloadApiUrl: downloadSettings.downloadApiUrl, }), - { - onMutate: ({ downloadId, deleted }) => { - const prevDownloads = queryClient.getQueryData([QueryKeys.DOWNLOADS]); - - if (deleted) { - queryClient.setQueryData( - [QueryKeys.DOWNLOADS], - (oldDownloads) => - oldDownloads && - oldDownloads.filter((download) => download.id !== downloadId) + + onMutate: ({ downloadId, deleted }, context) => { + const prevDownloads = context.client.getQueriesData({ + queryKey: QueryKeys.DOWNLOADS, + }); + + if (deleted) { + context.client.setQueriesData( + { queryKey: [QueryKeys.DOWNLOADS] }, + (oldDownloads) => + oldDownloads && + oldDownloads.filter((download) => download.id !== downloadId) + ); + } + + return { prevDownloads }; + }, + + onSuccess: async ( + _data, + { downloadId, deleted }, + _onMutateResult, + context + ) => { + if (!deleted) { + // download is restored (un-deleted), fetch the download info + const restoredDownload = await getDownload( + downloadId, + downloadSettings.facilityName, + downloadSettings.downloadApiUrl + ); + + if (restoredDownload) { + context.client.setQueriesData( + { queryKey: [QueryKeys.DOWNLOADS] }, + (downloads) => downloads && [...downloads, restoredDownload] ); } + } + }, - return () => - queryClient.setQueryData([QueryKeys.DOWNLOADS], prevDownloads); - }, - - onSuccess: async (_, { downloadId, deleted }) => { - if (!deleted) { - // download is restored (un-deleted), fetch the download info - const restoredDownload = await getDownload( - downloadId, - downloadSettings.facilityName, - downloadSettings.downloadApiUrl - ); + onError: (error: AxiosError, _, onMutateResult, context) => { + handleICATError(error); + if (onMutateResult) + context.client.setQueriesData( + { queryKey: [QueryKeys.DOWNLOADS] }, + onMutateResult.prevDownloads + ); + }, + + retry: (failureCount, error) => { + // if we get 431 we know this is an intermittent error so retry + return error.response?.status === 431 && failureCount < 3; + }, + }); +}; + +const buildQueryOffset = ( + filters: FiltersType, + sort: SortType, + facilityName: string +) => { + let queryOffset = `WHERE download.facilityName = '${facilityName}'`; + for (const [column, filter] of Object.entries(filters)) { + if (typeof filter === 'object') { + if (!Array.isArray(filter)) { + if ('startDate' in filter || 'endDate' in filter) { + const startDate = filter.startDate + ? `${filter.startDate}` + : '0001-01-01 00:00:00'; + const endDate = filter.endDate + ? `${filter.endDate}` + : '9999-12-31 23:59:00'; + + queryOffset += ` AND download.${column} BETWEEN {ts '${startDate}'} AND {ts '${endDate}'}`; + } - if (restoredDownload) { - queryClient.setQueryData( - [QueryKeys.DOWNLOADS], - (downloads) => downloads && [...downloads, restoredDownload] - ); + if ('type' in filter && filter.type) { + // As UPPER is used need to pass text filters in upper case to avoid case sensitivity + // also need to escape single quotes + const filterValue = + typeof filter.value === 'string' + ? filter.type !== 'exact' + ? (filter.value as string).toUpperCase().replaceAll("'", "''") + : filter.value.replaceAll("'", "''") + : filter.value; + + // use switch statement to ensure TS can detect we cover all cases + switch (filter.type) { + case 'include': + queryOffset += ` AND UPPER(download.${column}) LIKE CONCAT('%', '${filterValue}', '%')`; + break; + case 'exclude': + queryOffset += ` AND UPPER(download.${column}) NOT LIKE CONCAT('%', '${filterValue}', '%')`; + break; + case 'exact': + queryOffset += ` AND download.${column} = '${filterValue}'`; + break; + default: { + const exhaustiveCheck: never = filter.type; + throw new Error(`Unhandled text filter type: ${exhaustiveCheck}`); + } } } - }, + } + } + } - onError: (error, _, rollback) => { - handleICATError(error); - if (rollback) rollback(); - }, + queryOffset += ' ORDER BY'; + for (const [column, order] of Object.entries(sort)) { + queryOffset += ` download.${column} ${order},`; + } + queryOffset += ' download.id ASC'; - retry: (failureCount, error) => { - // if we get 431 we know this is an intermittent error so retry - return error.response?.status === 431 && failureCount < 3; - }, - } - ); + return queryOffset; }; /** @@ -326,29 +363,40 @@ export const useDownloadOrRestoreDownload = (): UseMutationResult< * @param initialQueryOffset The initial query offset for the list of downloads. */ export const useAdminDownloads = ({ - initialQueryOffset, + filters, + sort, }: { - initialQueryOffset: string; -}): UseInfiniteQueryResult => { + filters: FiltersType; + sort: SortType; +}) => { // Load the download settings for use const downloadSettings = React.useContext(DownloadSettingsContext); - return useInfiniteQuery( - [QueryKeys.ADMIN_DOWNLOADS, initialQueryOffset], - ({ pageParam = initialQueryOffset }) => + return useInfiniteQuery({ + queryKey: [ + QueryKeys.ADMIN_DOWNLOADS, + filters, + sort, + downloadSettings.facilityName, + downloadSettings.downloadApiUrl, + ], + queryFn: ({ pageParam }) => fetchAdminDownloads( { facilityName: downloadSettings.facilityName, downloadApiUrl: downloadSettings.downloadApiUrl, }, - pageParam + `${buildQueryOffset(filters, sort, downloadSettings.facilityName)} LIMIT ${pageParam.skip}, ${ + pageParam.limit + }` ), - { - onError: (error) => { - handleICATError(error); - }, - } - ); + getNextPageParam: (_lastPage, _allPages, lastPageParam) => ({ + skip: lastPageParam.skip + lastPageParam.limit, + limit: INFINITE_SCROLL_BATCH_SIZE, + }), + initialPageParam: { skip: 0, limit: 50 }, + meta: { icatError: true }, + }); }; export interface AdminDownloadDeletedParams { @@ -359,59 +407,55 @@ export interface AdminDownloadDeletedParams { /** * A React hook that provides a mutation function for deleting/restoring admin downloads. */ -export const useAdminDownloadDeleted = (): UseMutationResult< - void, - AxiosError, - AdminDownloadDeletedParams, - RollbackFunction -> => { - const queryClient = useQueryClient(); +export const useAdminDownloadDeleted = () => { // Load the download settings for use. const downloadSettings = React.useContext(DownloadSettingsContext); - return useMutation( - ({ downloadId, deleted }) => + return useMutation({ + mutationFn: ({ downloadId, deleted }: AdminDownloadDeletedParams) => adminDownloadDeleted(downloadId, deleted, { facilityName: downloadSettings.facilityName, downloadApiUrl: downloadSettings.downloadApiUrl, }), - { - onSuccess: async (_, { downloadId }) => { - const downloads = await fetchAdminDownloads( - { - facilityName: downloadSettings.facilityName, - downloadApiUrl: downloadSettings.downloadApiUrl, - }, - `WHERE download.id = ${downloadId}` + + onSuccess: async (_, { downloadId }, _onMutateResult, context) => { + const downloads = await fetchAdminDownloads( + { + facilityName: downloadSettings.facilityName, + downloadApiUrl: downloadSettings.downloadApiUrl, + }, + `WHERE download.id = ${downloadId}` + ); + if (downloads.length > 0) { + const updatedDownload = downloads[0]; + context.client.setQueriesData>( + { queryKey: [QueryKeys.ADMIN_DOWNLOADS], type: 'active' }, + (oldData) => + oldData && { + ...oldData, + pages: oldData.pages.map((page) => + page.map((download) => + download.id === updatedDownload.id + ? updatedDownload + : download + ) + ), + } ); - if (downloads.length > 0) { - const updatedDownload = downloads[0]; - queryClient.setQueryData>( - [QueryKeys.ADMIN_DOWNLOADS], - (oldData) => - oldData && { - ...oldData, - pages: oldData.pages.map((page) => - page.map((download) => - download.id === updatedDownload.id - ? updatedDownload - : download - ) - ), - } - ); - } - }, + } + }, - onError: (error) => { - handleICATError(error); - }, + onError: (error: AxiosError) => { + handleICATError(error); + }, - onSettled: () => { - queryClient.invalidateQueries([QueryKeys.ADMIN_DOWNLOADS]); - }, - } - ); + onSettled: (_data, _error, _variables, _onMutateResult, context) => { + context.client.invalidateQueries({ + queryKey: [QueryKeys.ADMIN_DOWNLOADS], + type: 'active', + }); + }, + }); }; /** @@ -422,90 +466,93 @@ export interface AdminUpdateDownloadStatusParams { status: DownloadStatus; } -export const useAdminUpdateDownloadStatus = (): UseMutationResult< - void, - AxiosError, - AdminUpdateDownloadStatusParams, - RollbackFunction -> => { - const queryClient = useQueryClient(); +export const useAdminUpdateDownloadStatus = () => { // Load the download settings for use. const downloadSettings = React.useContext(DownloadSettingsContext); - return useMutation( - ({ downloadId, status }) => + return useMutation({ + mutationFn: ({ downloadId, status }: AdminUpdateDownloadStatusParams) => adminDownloadStatus(downloadId, status, { facilityName: downloadSettings.facilityName, downloadApiUrl: downloadSettings.downloadApiUrl, }), - { - onMutate: ({ downloadId, status }) => { - const prevDownloads = queryClient.getQueryData([ - QueryKeys.ADMIN_DOWNLOADS, - ]); - - queryClient.setQueryData>( - [QueryKeys.ADMIN_DOWNLOADS], - (oldData) => - oldData && { - ...oldData, - pages: oldData.pages.map((page) => - page.map((download) => - download.id === downloadId - ? { ...download, status } - : download - ) - ), - } - ); - return () => - queryClient.setQueryData([QueryKeys.ADMIN_DOWNLOADS], prevDownloads); - }, + onMutate: ({ downloadId, status }, context) => { + const prevDownloads = context.client.getQueriesData< + InfiniteData + >({ + queryKey: [QueryKeys.ADMIN_DOWNLOADS], + type: 'active', + }); - onError: (error, _, rollback) => { - handleICATError(error); - if (rollback) rollback(); - }, + context.client.setQueriesData>( + { + queryKey: [QueryKeys.ADMIN_DOWNLOADS], + type: 'active', + }, + (oldData) => + oldData && { + ...oldData, + pages: oldData.pages.map((page) => + page.map((download) => + download.id === downloadId ? { ...download, status } : download + ) + ), + } + ); - onSettled: () => { - queryClient.invalidateQueries([QueryKeys.ADMIN_DOWNLOADS]); - }, - } - ); + return { prevDownloads }; + }, + + onError: (error: AxiosError, _, onMutateResult, context) => { + handleICATError(error); + if (onMutateResult) { + onMutateResult.prevDownloads.forEach(([queryKey, prevDownload]) => { + context.client.setQueryData>( + queryKey, + prevDownload + ); + }); + } + }, + + onSettled: (_data, _error, _variables, _onMutateResult, context) => { + context.client.invalidateQueries({ + queryKey: [QueryKeys.ADMIN_DOWNLOADS], + type: 'active', + }); + }, + }); }; /** * Queries the progress of a {@link Download}. * @param download The {@link Download} that this query should query the restore progress of. - * @param queryOptions Optional `useQuery` option override. + * @param idsUrl The idsUrl to query. + * @param enabled Whether to disable the query. */ -export const useDownloadPercentageComplete = ({ +export const useDownloadPercentageComplete = ({ download, idsUrl, - ...queryOptions -}: { download: Download; idsUrl: string } & UseQueryOptions< - DownloadProgress, - AxiosError, - T, - string[] ->): UseQueryResult => { + enabled, +}: { + download: Download; + idsUrl: string; + enabled?: boolean; +}) => { const preparedId = download.preparedId; - return useQuery( - [QueryKeys.DOWNLOAD_PROGRESS, preparedId ?? ''], // undefined preparedId is handled in downloadProgressIndicator & disables the query anyway - () => + return useQuery({ + queryKey: [QueryKeys.DOWNLOAD_PROGRESS, preparedId ?? '', idsUrl], + // undefined preparedId is handled in downloadProgressIndicator & disables the query anyway + queryFn: () => getPercentageComplete({ preparedId: preparedId, settings: { idsUrl }, }), - { - onError: (error) => { - handleICATError(error, false); - }, - ...queryOptions, - } - ); + meta: { icatError: true, broadcastCondition: () => false }, + enabled, + }); }; /** @@ -513,90 +560,84 @@ export const useDownloadPercentageComplete = ({ * @param cart The {@link Cart} to mint * @param doiMetadata The required metadata for the DOI */ -export const useMintDraftCart = (): UseMutationResult< - DOIDraftResponse, - AxiosError<{ - detail: { msg: string }[] | string; - }>, - { cart: DownloadCartItem[]; doiMetadata: DOIMetadata } -> => { +export const useMintDraftCart = () => { const settings = React.useContext(DownloadSettingsContext); - return useMutation( - ({ cart, doiMetadata }) => { + return useMutation({ + mutationFn: ({ + cart, + doiMetadata, + }: { + cart: DownloadCartItem[]; + doiMetadata: DOIMetadata; + }) => { return mintDraftCart(cart, doiMetadata, settings); }, - { - onError: (error) => { - handleDOIAPIError(error, undefined, undefined, true, true); - }, - } - ); + onError: ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + handleDOIAPIError(error, true, true); + }, + }); }; /** * Publishes a draft data publication * @param dataPublicationId The {@link DataPublication} to publish */ -export const usePublishDraft = (): UseMutationResult< - DOIResponse, - AxiosError<{ - detail: { msg: string }[] | string; - }>, - string -> => { +export const usePublishDraft = () => { const settings = React.useContext(DownloadSettingsContext); - return useMutation( - (dataPublicationId: string) => { + return useMutation({ + mutationFn: (dataPublicationId: string) => { return publishDraftDOI(dataPublicationId, settings); }, - { - onError: handleDOIAPIError, - } - ); + + onError: ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + handleDOIAPIError(error, true, true); + }, + }); }; /** * Deletes a draft data publication * @param dataPublicationId The {@link DataPublication} to publish */ -export const useDeleteDraft = (): UseMutationResult< - void, - AxiosError<{ - detail: { msg: string }[] | string; - }>, - string -> => { +export const useDeleteDraft = () => { const settings = React.useContext(DownloadSettingsContext); - return useMutation( - (dataPublicationId: string) => { + return useMutation({ + mutationFn: (dataPublicationId: string) => { return deleteDraftDOI(dataPublicationId, settings); }, - { - onError: (error) => { - handleDOIAPIError(error, undefined, undefined, true, true); - }, - } - ); + + onError: ( + error: AxiosError<{ + detail: { msg: string }[] | string; + }> + ) => { + handleDOIAPIError(error, true, true); + }, + }); }; /** * Gets the total list of users associated with each item in the cart * @param cart The {@link Cart} that we're getting the users for */ -export const useCartUsers = ( - cart?: DownloadCartItem[] -): UseQueryResult => { +export const useCartUsers = (cart?: DownloadCartItem[]) => { const settings = React.useContext(DownloadSettingsContext); - return useQuery( - ['cartUsers', cart], - () => getCartUsers(cart ?? [], settings), - { - onError: handleICATError, - staleTime: Infinity, - } - ); + return useQuery({ + queryKey: ['cartUsers', cart, settings], + queryFn: () => getCartUsers(cart ?? [], settings), + meta: { icatError: true }, + staleTime: Infinity, + }); }; diff --git a/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx index 20efff976..ef63a7a0e 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartItemLink.component.tsx @@ -1,8 +1,8 @@ import { Link as MuiLink } from '@mui/material'; -import type { DownloadCartItem } from 'datagateway-common'; import { useQuery } from '@tanstack/react-query'; -import { Link } from 'react-router-dom'; +import type { DownloadCartItem } from 'datagateway-common'; import pLimit from 'p-limit'; +import { Link } from 'react-router-dom'; type LinkBuilder = () => Promise; @@ -17,7 +17,10 @@ function DownloadCartItemLink({ cartItem, linkBuilder, }: DownloadCartItemLinkProps): JSX.Element { - const { data: link } = useQuery(['cartItemLink', cartItem.id], { + const { data: link } = useQuery({ + // link builder is not serialisable and can't be passed in query key + // eslint-disable-next-line @tanstack/query/exhaustive-deps + queryKey: ['cartItemLink', cartItem.id], queryFn: () => cartLinkLimit(linkBuilder), staleTime: Infinity, }); diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx index 383d33159..7f61f2c1e 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.test.tsx @@ -53,12 +53,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); const renderComponent = (): RenderResult & { history: MemoryHistory } => { diff --git a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx index e74044dc7..b753c4cba 100644 --- a/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx +++ b/packages/datagateway-download/src/downloadCart/downloadCartTable.component.tsx @@ -77,12 +77,12 @@ const DownloadCartTable: React.FC = ( useDownloadTypes(facilityName, downloadApiUrl); const { data: isTwoLevel } = useIsTwoLevel(); const { mutate: removeDownloadCartItem } = useRemoveEntityFromCart(); - const { mutate: removeAllDownloadCartItems, isLoading: removingAll } = + const { mutate: removeAllDownloadCartItems, isPending: removingAll } = useRemoveAllFromCart(); const { data: cartItems, isFetching: isFetchingCart } = useCart(); const { data: mintable, - isLoading: cartMintabilityLoading, + isPending: cartMintabilityLoading, error: mintableError, } = useIsCartMintable(cartItems, doiMinterUrl); @@ -107,7 +107,7 @@ const DownloadCartTable: React.FC = ( }, [fileSizesAndCounts]); const fileSizesAndCountsLoading = fileSizesAndCounts.some( - (query) => query?.isLoading + (query) => query?.isPending ); const [t] = useTranslation(); @@ -368,7 +368,7 @@ const DownloadCartTable: React.FC = ( (query) => query.data?.fileSize === 0 || query.data?.fileCount === 0 ); - const isLoading = isFetchingCart; + const isPending = isFetchingCart; return ( <> @@ -429,7 +429,7 @@ const DownloadCartTable: React.FC = (
{/* Show loading progress if data is still being loaded */} - {isLoading && ( + {isPending && ( @@ -447,7 +447,7 @@ const DownloadCartTable: React.FC = ( (totalSizeMax && totalSize > totalSizeMax) ? ' - 2rem' : '' - }${isLoading ? ' - 4px' : ''} - (1.75 * 0.875rem + 12px))`, + }${isPending ? ' - 4px' : ''} - (1.75 * 0.875rem + 12px))`, minHeight: 230, overflowX: 'auto', // handle the highlight of unmintable entities @@ -472,7 +472,7 @@ const DownloadCartTable: React.FC = ( sort={sort} onSort={onSort} data={sortedAndFilteredData ?? []} - loading={isLoading} + loading={isPending} actions={actions} /> diff --git a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.test.ts b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.test.ts index 066b77938..a4d76318a 100644 --- a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.test.ts +++ b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.test.ts @@ -1,59 +1,59 @@ -import axios from 'axios'; -import { mockDatafiles, mockedSettings } from '../../testData'; -import buildDatafileUrl from './buildDatafileUrl'; - -describe('buildDatafileUrl', () => { - beforeEach(() => { - axios.get = vi.fn().mockResolvedValue({ - data: [mockDatafiles[0]], - }); - }); - - it('should return a generic URL to the parent dataset of the datafile', async () => { - const url = await buildDatafileUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: mockedSettings.facilityName, - datafileId: 70, - }); - - expect(url).toBe('/browse/investigation/58/dataset/856/datafile'); - }); - - it('should return an ISIS URL to the parent dataset of the datafile', async () => { - const url = await buildDatafileUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - datafileId: 70, - }); - - expect(url).toBe( - '/browse/instrument/937/facilityCycle/402/investigation/58/dataset/856/datafile' - ); - }); - - it('should return a DLS URL to the parent dataset of the datafile', async () => { - const url = await buildDatafileUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: 'DLS', - datafileId: 70, - }); - - expect(url).toBe( - '/browse/proposal/investigation news/investigation/58/dataset/856/datafile' - ); - }); - - it('should return null if the parent dataset of the datafile cannot be fetched', async () => { - axios.get = vi.fn().mockResolvedValue({ - data: [], - }); - - const url = await buildDatafileUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: 'DLS', - datafileId: 70, - }); - - expect(url).toBeNull(); - }); -}); +import axios from 'axios'; +import { mockDatafiles, mockedSettings } from '../../testData'; +import buildDatafileUrl from './buildDatafileUrl'; + +describe('buildDatafileUrl', () => { + beforeEach(() => { + axios.get = vi.fn().mockResolvedValue({ + data: [mockDatafiles[0]], + }); + }); + + it('should return a generic URL to the parent dataset of the datafile', async () => { + const url = await buildDatafileUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: mockedSettings.facilityName, + datafileId: 70, + }); + + expect(url).toBe('/browse/investigation/58/dataset/856/datafile'); + }); + + it('should return an ISIS URL to the parent dataset of the datafile', async () => { + const url = await buildDatafileUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + datafileId: 70, + }); + + expect(url).toBe( + '/browse/instrument/937/facilityCycle/402/investigation/58/dataset/856/datafile' + ); + }); + + it('should return a DLS URL to the parent dataset of the datafile', async () => { + const url = await buildDatafileUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: 'DLS', + datafileId: 70, + }); + + expect(url).toBe( + '/browse/proposal/investigation news/investigation/58/dataset/856/datafile' + ); + }); + + it('should return null if the parent dataset of the datafile cannot be fetched', async () => { + axios.get = vi.fn().mockResolvedValue({ + data: [], + }); + + const url = await buildDatafileUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: 'DLS', + datafileId: 70, + }); + + expect(url).toBeNull(); + }); +}); diff --git a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.ts b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.ts index 1ffd2236e..f9bdab949 100644 --- a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.ts +++ b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatafileUrl.ts @@ -1,67 +1,67 @@ -import { type Datafile, fetchDatafiles } from 'datagateway-common'; -import { buildDatasetUrl } from '../urlBuilders'; - -async function fetchDatafile({ - apiUrl, - facilityName, - datafileId, -}: { - apiUrl: string; - facilityName: string; - datafileId: Datafile['id']; -}): Promise { - let includeField: string[]; - switch (facilityName) { - case 'ISIS': - includeField = [ - 'dataset.investigation.investigationInstruments.instrument', - 'dataset.investigation.investigationFacilityCycles.facilityCycle', - ]; - break; - default: - includeField = ['dataset.investigation']; - break; - } - - const datafiles = await fetchDatafiles(apiUrl, { sort: {}, filters: {} }, [ - { - filterType: 'where', - filterValue: JSON.stringify({ id: { eq: datafileId } }), - }, - { - filterType: 'include', - filterValue: JSON.stringify(includeField), - }, - ]); - - return datafiles[0] ?? null; -} - -/** - * Given either a dataset ID or a {@link Datafile} object, constructs a URL to the {@link Datafile}. - * The URL points to the dataset table the {@link Datafile} belongs to. - * - * @returns The URL to the dataset table that the datafile belongs to, - * or `null` if the URL cannot be constructed due to missing info. - */ -async function buildDatafileUrl({ - apiUrl, - facilityName, - datafileId, -}: { - datafileId: Datafile['id']; - apiUrl: string; - facilityName: string; -}): Promise { - const datafile = await fetchDatafile({ apiUrl, facilityName, datafileId }); - const dataset = datafile?.dataset; - if (!dataset) return null; - - return buildDatasetUrl({ - apiUrl, - facilityName, - dataset, - }); -} - -export default buildDatafileUrl; +import { type Datafile, fetchDatafiles } from 'datagateway-common'; +import { buildDatasetUrl } from '../urlBuilders'; + +async function fetchDatafile({ + apiUrl, + facilityName, + datafileId, +}: { + apiUrl: string; + facilityName: string; + datafileId: Datafile['id']; +}): Promise { + let includeField: string[]; + switch (facilityName) { + case 'ISIS': + includeField = [ + 'dataset.investigation.investigationInstruments.instrument', + 'dataset.investigation.investigationFacilityCycles.facilityCycle', + ]; + break; + default: + includeField = ['dataset.investigation']; + break; + } + + const datafiles = await fetchDatafiles(apiUrl, { sort: {}, filters: {} }, [ + { + filterType: 'where', + filterValue: JSON.stringify({ id: { eq: datafileId } }), + }, + { + filterType: 'include', + filterValue: JSON.stringify(includeField), + }, + ]); + + return datafiles[0] ?? null; +} + +/** + * Given either a dataset ID or a {@link Datafile} object, constructs a URL to the {@link Datafile}. + * The URL points to the dataset table the {@link Datafile} belongs to. + * + * @returns The URL to the dataset table that the datafile belongs to, + * or `null` if the URL cannot be constructed due to missing info. + */ +async function buildDatafileUrl({ + apiUrl, + facilityName, + datafileId, +}: { + datafileId: Datafile['id']; + apiUrl: string; + facilityName: string; +}): Promise { + const datafile = await fetchDatafile({ apiUrl, facilityName, datafileId }); + const dataset = datafile?.dataset; + if (!dataset) return null; + + return buildDatasetUrl({ + apiUrl, + facilityName, + dataset, + }); +} + +export default buildDatafileUrl; diff --git a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.test.ts b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.test.ts index 9f68302c4..1399fa480 100644 --- a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.test.ts +++ b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.test.ts @@ -1,141 +1,141 @@ -import { mockDatasets, mockedSettings } from '../../testData'; -import axios from 'axios'; -import type { Dataset } from 'datagateway-common'; -import buildDatasetUrl from './buildDatasetUrl'; - -describe('buildDatasetUrl', () => { - describe('given a dataset ID', () => { - let dataset: Dataset; - - beforeEach(() => { - dataset = mockDatasets[0]; - axios.get = vi.fn().mockResolvedValue({ - data: [dataset], - }); - }); - - it('should return the generic URL to it', async () => { - const url = await buildDatasetUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: mockedSettings.facilityName, - datasetId: 856, - }); - - expect(url).toBe('/browse/investigation/58/dataset/856/datafile'); - }); - - it('should return the ISIS URL to it', async () => { - const url = await buildDatasetUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - datasetId: 856, - }); - - expect(url).toBe( - '/browse/instrument/937/facilityCycle/402/investigation/58/dataset/856/datafile' - ); - }); - - it('should return the DLS URL to it', async () => { - const url = await buildDatasetUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: 'DLS', - datasetId: 856, - }); - - expect(url).toBe( - '/browse/proposal/investigation news/investigation/58/dataset/856/datafile' - ); - }); - }); - - describe('given a dataset object', () => { - const dataset = mockDatasets[1]; - - it('should return the generic URL to it', async () => { - const url = await buildDatasetUrl({ - dataset, - apiUrl: mockedSettings.apiUrl, - facilityName: mockedSettings.facilityName, - }); - - expect(url).toBe('/browse/investigation/993/dataset/535/datafile'); - }); - - it('should return the ISIS URL to it', async () => { - const url = await buildDatasetUrl({ - dataset, - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - }); - - expect(url).toBe( - '/browse/instrument/927/facilityCycle/402/investigation/993/dataset/535/datafile' - ); - }); - - it('should return the DLS URL to it', async () => { - const url = await buildDatasetUrl({ - dataset, - apiUrl: mockedSettings.apiUrl, - facilityName: 'DLS', - }); - - expect(url).toBe( - '/browse/proposal/investigation inn/investigation/993/dataset/535/datafile' - ); - }); - }); - - it('should return null if neither a dataset object nor a dataset id is provided', async () => { - const url = await buildDatasetUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: mockedSettings.facilityName, - }); - - expect(url).toBeNull(); - }); - - it('should return null if the dataset object cannot be fetched', async () => { - axios.get = vi.fn().mockResolvedValue({ - data: [], - }); - - const url = await buildDatasetUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: mockedSettings.facilityName, - datasetId: 856, - }); - - expect(url).toBeNull(); - }); - - it('should return null if the parent investigation of the dataset is not fetched', async () => { - const { investigation, ...dataset } = mockDatasets[0]; - - axios.get = vi.fn().mockResolvedValue({ - data: [dataset], - }); - - const url = await buildDatasetUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: mockedSettings.facilityName, - datasetId: 856, - }); - - expect(url).toBeNull(); - }); - - it('should return null if the parent investigation URL cannot be constructed', async () => { - const dataset = { ...mockDatasets[0] }; - delete dataset.investigation?.investigationInstruments; - - const url = await buildDatasetUrl({ - dataset, - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - }); - - expect(url).toBeNull(); - }); -}); +import { mockDatasets, mockedSettings } from '../../testData'; +import axios from 'axios'; +import type { Dataset } from 'datagateway-common'; +import buildDatasetUrl from './buildDatasetUrl'; + +describe('buildDatasetUrl', () => { + describe('given a dataset ID', () => { + let dataset: Dataset; + + beforeEach(() => { + dataset = mockDatasets[0]; + axios.get = vi.fn().mockResolvedValue({ + data: [dataset], + }); + }); + + it('should return the generic URL to it', async () => { + const url = await buildDatasetUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: mockedSettings.facilityName, + datasetId: 856, + }); + + expect(url).toBe('/browse/investigation/58/dataset/856/datafile'); + }); + + it('should return the ISIS URL to it', async () => { + const url = await buildDatasetUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + datasetId: 856, + }); + + expect(url).toBe( + '/browse/instrument/937/facilityCycle/402/investigation/58/dataset/856/datafile' + ); + }); + + it('should return the DLS URL to it', async () => { + const url = await buildDatasetUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: 'DLS', + datasetId: 856, + }); + + expect(url).toBe( + '/browse/proposal/investigation news/investigation/58/dataset/856/datafile' + ); + }); + }); + + describe('given a dataset object', () => { + const dataset = mockDatasets[1]; + + it('should return the generic URL to it', async () => { + const url = await buildDatasetUrl({ + dataset, + apiUrl: mockedSettings.apiUrl, + facilityName: mockedSettings.facilityName, + }); + + expect(url).toBe('/browse/investigation/993/dataset/535/datafile'); + }); + + it('should return the ISIS URL to it', async () => { + const url = await buildDatasetUrl({ + dataset, + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + }); + + expect(url).toBe( + '/browse/instrument/927/facilityCycle/402/investigation/993/dataset/535/datafile' + ); + }); + + it('should return the DLS URL to it', async () => { + const url = await buildDatasetUrl({ + dataset, + apiUrl: mockedSettings.apiUrl, + facilityName: 'DLS', + }); + + expect(url).toBe( + '/browse/proposal/investigation inn/investigation/993/dataset/535/datafile' + ); + }); + }); + + it('should return null if neither a dataset object nor a dataset id is provided', async () => { + const url = await buildDatasetUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: mockedSettings.facilityName, + }); + + expect(url).toBeNull(); + }); + + it('should return null if the dataset object cannot be fetched', async () => { + axios.get = vi.fn().mockResolvedValue({ + data: [], + }); + + const url = await buildDatasetUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: mockedSettings.facilityName, + datasetId: 856, + }); + + expect(url).toBeNull(); + }); + + it('should return null if the parent investigation of the dataset is not fetched', async () => { + const { investigation, ...dataset } = mockDatasets[0]; + + axios.get = vi.fn().mockResolvedValue({ + data: [dataset], + }); + + const url = await buildDatasetUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: mockedSettings.facilityName, + datasetId: 856, + }); + + expect(url).toBeNull(); + }); + + it('should return null if the parent investigation URL cannot be constructed', async () => { + const dataset = { ...mockDatasets[0] }; + delete dataset.investigation?.investigationInstruments; + + const url = await buildDatasetUrl({ + dataset, + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + }); + + expect(url).toBeNull(); + }); +}); diff --git a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.ts b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.ts index cea868e78..16a82ed56 100644 --- a/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.ts +++ b/packages/datagateway-download/src/downloadCart/urlBuilders/buildDatasetUrl.ts @@ -1,87 +1,87 @@ -import { type Dataset, fetchDatasets } from 'datagateway-common'; -import { buildInvestigationUrl } from '../urlBuilders'; - -async function fetchDataset({ - apiUrl, - facilityName, - datasetId, -}: { - apiUrl: string; - facilityName: string; - datasetId: Dataset['id']; -}): Promise { - let includeField: string[]; - switch (facilityName) { - case 'ISIS': - includeField = [ - 'investigation.investigationInstruments.instrument', - 'investigation.investigationFacilityCycles.facilityCycle', - ]; - break; - default: - includeField = ['investigation']; - break; - } - - const datasets = await fetchDatasets(apiUrl, { sort: {}, filters: {} }, [ - { - filterType: 'where', - filterValue: JSON.stringify({ id: { eq: datasetId } }), - }, - { - filterType: 'include', - filterValue: JSON.stringify(includeField), - }, - ]); - - return datasets[0] ?? null; -} - -/** - * Given either a dataset ID or a {@link Dataset} object, constructs a URL to the {@link Dataset}. - * - * If providing a {@link Dataset} object, the {@link Dataset.investigation} has to be present, - * and the {@link Investigation} object has to have the {@link Investigation.investigationInstruments} field. - * - * @returns The URL to the dataset table, or `null` if the URL cannot be constructed due to missing info. - */ -async function buildDatasetUrl({ - apiUrl, - facilityName, - datasetId, - dataset: providedDataset, -}: { - datasetId?: Dataset['id']; - dataset?: Dataset; - apiUrl: string; - facilityName: string; -}): Promise { - if (!datasetId && !providedDataset) { - // if neither a dataset object nor a dataset ID is provided, nothing can be built - // return nothing. - return null; - } - - let dataset: Dataset | null; - if (providedDataset) { - dataset = providedDataset; - } else if (datasetId) { - dataset = await fetchDataset({ apiUrl, facilityName, datasetId }); - } else { - return null; - } - - const investigation = dataset?.investigation; - if (!dataset || !investigation) return null; - - const prefixUrl = await buildInvestigationUrl({ - apiUrl, - facilityName, - investigation, - }); - if (!prefixUrl) return null; - - return `${prefixUrl}/${dataset.id}/datafile`; -} - -export default buildDatasetUrl; +import { type Dataset, fetchDatasets } from 'datagateway-common'; +import { buildInvestigationUrl } from '../urlBuilders'; + +async function fetchDataset({ + apiUrl, + facilityName, + datasetId, +}: { + apiUrl: string; + facilityName: string; + datasetId: Dataset['id']; +}): Promise { + let includeField: string[]; + switch (facilityName) { + case 'ISIS': + includeField = [ + 'investigation.investigationInstruments.instrument', + 'investigation.investigationFacilityCycles.facilityCycle', + ]; + break; + default: + includeField = ['investigation']; + break; + } + + const datasets = await fetchDatasets(apiUrl, { sort: {}, filters: {} }, [ + { + filterType: 'where', + filterValue: JSON.stringify({ id: { eq: datasetId } }), + }, + { + filterType: 'include', + filterValue: JSON.stringify(includeField), + }, + ]); + + return datasets[0] ?? null; +} + +/** + * Given either a dataset ID or a {@link Dataset} object, constructs a URL to the {@link Dataset}. + * + * If providing a {@link Dataset} object, the {@link Dataset.investigation} has to be present, + * and the {@link Investigation} object has to have the {@link Investigation.investigationInstruments} field. + * + * @returns The URL to the dataset table, or `null` if the URL cannot be constructed due to missing info. + */ +async function buildDatasetUrl({ + apiUrl, + facilityName, + datasetId, + dataset: providedDataset, +}: { + datasetId?: Dataset['id']; + dataset?: Dataset; + apiUrl: string; + facilityName: string; +}): Promise { + if (!datasetId && !providedDataset) { + // if neither a dataset object nor a dataset ID is provided, nothing can be built + // return nothing. + return null; + } + + let dataset: Dataset | null; + if (providedDataset) { + dataset = providedDataset; + } else if (datasetId) { + dataset = await fetchDataset({ apiUrl, facilityName, datasetId }); + } else { + return null; + } + + const investigation = dataset?.investigation; + if (!dataset || !investigation) return null; + + const prefixUrl = await buildInvestigationUrl({ + apiUrl, + facilityName, + investigation, + }); + if (!prefixUrl) return null; + + return `${prefixUrl}/${dataset.id}/datafile`; +} + +export default buildDatasetUrl; diff --git a/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.test.ts b/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.test.ts index d99f4c018..d35aa227a 100644 --- a/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.test.ts +++ b/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.test.ts @@ -1,157 +1,157 @@ -import buildInvestigationUrl from './buildInvestigationUrl'; -import { mockedSettings, mockInvestigations } from '../../testData'; -import axios from 'axios'; -import type { Investigation } from 'datagateway-common'; - -describe('buildInvestigationUrl', () => { - describe('given an investigation object', () => { - let investigation: Investigation; - - beforeEach(() => { - investigation = mockInvestigations[0]; - axios.get = vi.fn().mockResolvedValue({ - data: [investigation], - }); - }); - - it('should return a generic URL to it', async () => { - const url = await buildInvestigationUrl({ - investigation, - apiUrl: mockedSettings.apiUrl, - facilityName: 'SVELTE', - }); - - expect(url).toBe('/browse/investigation/58/dataset'); - }); - - it('should return an ISIS URL to it', async () => { - const url = await buildInvestigationUrl({ - investigation, - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - }); - - expect(url).toBe( - '/browse/instrument/937/facilityCycle/402/investigation/58/dataset' - ); - }); - - it('should return a DLS URL to it', async () => { - const url = await buildInvestigationUrl({ - investigation, - apiUrl: mockedSettings.apiUrl, - facilityName: 'DLS', - }); - - expect(url).toBe( - '/browse/proposal/investigation news/investigation/58/dataset' - ); - }); - }); - - describe('given an investigation id', () => { - let investigation: Investigation; - - beforeEach(() => { - investigation = mockInvestigations[1]; - axios.get = vi.fn().mockResolvedValue({ - data: [investigation], - }); - }); - - it('should return a generic URL to it', async () => { - const url = await buildInvestigationUrl({ - investigationId: 993, - apiUrl: mockedSettings.apiUrl, - facilityName: 'SVELTE', - }); - - expect(url).toBe('/browse/investigation/993/dataset'); - }); - - it('should return an ISIS URL to it', async () => { - const url = await buildInvestigationUrl({ - investigationId: 993, - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - }); - - expect(url).toBe( - '/browse/instrument/927/facilityCycle/402/investigation/993/dataset' - ); - }); - - it('should return a DLS URL to it', async () => { - const url = await buildInvestigationUrl({ - investigationId: 993, - apiUrl: mockedSettings.apiUrl, - facilityName: 'DLS', - }); - - expect(url).toBe( - '/browse/proposal/investigation inn/investigation/993/dataset' - ); - }); - }); - - it('should return null if neither an investigation object or an investigation id is provided', async () => { - const url = await buildInvestigationUrl({ - apiUrl: mockedSettings.apiUrl, - facilityName: mockedSettings.facilityName, - }); - - expect(url).toBeNull(); - }); - - it('should return null if the associated instruments for the investigation cannot be fetched', async () => { - const { investigationInstruments, ...investigation } = - mockInvestigations[0]; - - axios.get = vi.fn().mockResolvedValue({ - data: [investigation], - }); - - const url = await buildInvestigationUrl({ - investigationId: 993, - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - }); - - expect(url).toBeNull(); - }); - - it('should return null if the investigation object does not belong to any facility cycle', async () => { - axios.get = vi.fn().mockResolvedValue({ - data: [ - { - ...mockInvestigations[0], - startDate: '1999-03-09T08:19:55Z', - endDate: '1999-03-19T08:19:55Z', - investigationFacilityCycles: null, - }, - ], - }); - - const url = await buildInvestigationUrl({ - investigationId: 993, - apiUrl: mockedSettings.apiUrl, - facilityName: 'ISIS', - }); - - expect(url).toBeNull(); - }); - - it('should return null if the investigation object cannot be fetched', async () => { - axios.get = vi.fn().mockResolvedValue({ - data: [], - }); - - const url = await buildInvestigationUrl({ - investigationId: 993, - apiUrl: mockedSettings.apiUrl, - facilityName: 'DLS', - }); - - expect(url).toBeNull(); - }); -}); +import buildInvestigationUrl from './buildInvestigationUrl'; +import { mockedSettings, mockInvestigations } from '../../testData'; +import axios from 'axios'; +import type { Investigation } from 'datagateway-common'; + +describe('buildInvestigationUrl', () => { + describe('given an investigation object', () => { + let investigation: Investigation; + + beforeEach(() => { + investigation = mockInvestigations[0]; + axios.get = vi.fn().mockResolvedValue({ + data: [investigation], + }); + }); + + it('should return a generic URL to it', async () => { + const url = await buildInvestigationUrl({ + investigation, + apiUrl: mockedSettings.apiUrl, + facilityName: 'SVELTE', + }); + + expect(url).toBe('/browse/investigation/58/dataset'); + }); + + it('should return an ISIS URL to it', async () => { + const url = await buildInvestigationUrl({ + investigation, + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + }); + + expect(url).toBe( + '/browse/instrument/937/facilityCycle/402/investigation/58/dataset' + ); + }); + + it('should return a DLS URL to it', async () => { + const url = await buildInvestigationUrl({ + investigation, + apiUrl: mockedSettings.apiUrl, + facilityName: 'DLS', + }); + + expect(url).toBe( + '/browse/proposal/investigation news/investigation/58/dataset' + ); + }); + }); + + describe('given an investigation id', () => { + let investigation: Investigation; + + beforeEach(() => { + investigation = mockInvestigations[1]; + axios.get = vi.fn().mockResolvedValue({ + data: [investigation], + }); + }); + + it('should return a generic URL to it', async () => { + const url = await buildInvestigationUrl({ + investigationId: 993, + apiUrl: mockedSettings.apiUrl, + facilityName: 'SVELTE', + }); + + expect(url).toBe('/browse/investigation/993/dataset'); + }); + + it('should return an ISIS URL to it', async () => { + const url = await buildInvestigationUrl({ + investigationId: 993, + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + }); + + expect(url).toBe( + '/browse/instrument/927/facilityCycle/402/investigation/993/dataset' + ); + }); + + it('should return a DLS URL to it', async () => { + const url = await buildInvestigationUrl({ + investigationId: 993, + apiUrl: mockedSettings.apiUrl, + facilityName: 'DLS', + }); + + expect(url).toBe( + '/browse/proposal/investigation inn/investigation/993/dataset' + ); + }); + }); + + it('should return null if neither an investigation object or an investigation id is provided', async () => { + const url = await buildInvestigationUrl({ + apiUrl: mockedSettings.apiUrl, + facilityName: mockedSettings.facilityName, + }); + + expect(url).toBeNull(); + }); + + it('should return null if the associated instruments for the investigation cannot be fetched', async () => { + const { investigationInstruments, ...investigation } = + mockInvestigations[0]; + + axios.get = vi.fn().mockResolvedValue({ + data: [investigation], + }); + + const url = await buildInvestigationUrl({ + investigationId: 993, + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + }); + + expect(url).toBeNull(); + }); + + it('should return null if the investigation object does not belong to any facility cycle', async () => { + axios.get = vi.fn().mockResolvedValue({ + data: [ + { + ...mockInvestigations[0], + startDate: '1999-03-09T08:19:55Z', + endDate: '1999-03-19T08:19:55Z', + investigationFacilityCycles: null, + }, + ], + }); + + const url = await buildInvestigationUrl({ + investigationId: 993, + apiUrl: mockedSettings.apiUrl, + facilityName: 'ISIS', + }); + + expect(url).toBeNull(); + }); + + it('should return null if the investigation object cannot be fetched', async () => { + axios.get = vi.fn().mockResolvedValue({ + data: [], + }); + + const url = await buildInvestigationUrl({ + investigationId: 993, + apiUrl: mockedSettings.apiUrl, + facilityName: 'DLS', + }); + + expect(url).toBeNull(); + }); +}); diff --git a/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.ts b/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.ts index 56c3af7c7..8c82257c8 100644 --- a/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.ts +++ b/packages/datagateway-download/src/downloadCart/urlBuilders/buildInvestigationUrl.ts @@ -1,107 +1,107 @@ -import { - fetchInvestigations, - type AdditionalFilters, - type Investigation, -} from 'datagateway-common'; - -async function fetchInvestigation({ - apiUrl, - facilityName, - investigationId, -}: { - apiUrl: string; - facilityName: string; - investigationId: number; -}): Promise { - const filters: AdditionalFilters = [ - { - filterType: 'where', - filterValue: JSON.stringify({ id: { eq: investigationId } }), - }, - ]; - - if (facilityName === 'ISIS') { - filters.push({ - filterType: 'include', - filterValue: JSON.stringify([ - 'investigationInstruments.instrument', - 'investigationFacilityCycles.facilityCycle', - ]), - }); - } - - const investigations = await fetchInvestigations( - apiUrl, - { sort: {}, filters: {} }, - filters - ); - return investigations[0] ?? null; -} - -/** - * Given either an investigation ID or an {@link Investigation} object, constructs a link to the {@link Investigation}. - * - * If providing an {@link Investigation} object, the {@link Investigation.investigationInstruments} field has to be present. - * - * @returns A URL to the investigation table, or `null` if the URL cannot be constructed due to missing info. - */ -async function buildInvestigationUrl({ - apiUrl, - facilityName, - investigation: providedInvestigation, - investigationId, -}: { - investigation?: Investigation; - investigationId?: Investigation['id']; - apiUrl: string; - facilityName: string; -}): Promise { - if (!investigationId && !providedInvestigation) { - // if neither an investigation object nor an investigation ID is provided, nothing can be built - // return nothing. - return null; - } - - if (facilityName !== 'ISIS' && facilityName !== 'DLS') { - if (investigationId) { - return `/browse/investigation/${investigationId}/dataset`; - } - if (providedInvestigation) { - return `/browse/investigation/${providedInvestigation.id}/dataset`; - } - return null; - } - - let investigation: Investigation | null; - if (providedInvestigation) { - investigation = providedInvestigation; - } else if (investigationId) { - investigation = await fetchInvestigation({ - apiUrl, - facilityName, - investigationId, - }); - } else { - return null; - } - - if (!investigation) return null; - - switch (facilityName) { - case 'ISIS': { - const instrument = - investigation?.investigationInstruments?.[0]?.instrument; - if (!instrument) return null; - - const facilityCycle = - investigation?.investigationFacilityCycles?.[0]?.facilityCycle; - if (!facilityCycle) return null; - - return `/browse/instrument/${instrument.id}/facilityCycle/${facilityCycle.id}/investigation/${investigation.id}/dataset`; - } - case 'DLS': - return `/browse/proposal/${investigation.name}/investigation/${investigation.id}/dataset`; - } -} - -export default buildInvestigationUrl; +import { + fetchInvestigations, + type AdditionalFilters, + type Investigation, +} from 'datagateway-common'; + +async function fetchInvestigation({ + apiUrl, + facilityName, + investigationId, +}: { + apiUrl: string; + facilityName: string; + investigationId: number; +}): Promise { + const filters: AdditionalFilters = [ + { + filterType: 'where', + filterValue: JSON.stringify({ id: { eq: investigationId } }), + }, + ]; + + if (facilityName === 'ISIS') { + filters.push({ + filterType: 'include', + filterValue: JSON.stringify([ + 'investigationInstruments.instrument', + 'investigationFacilityCycles.facilityCycle', + ]), + }); + } + + const investigations = await fetchInvestigations( + apiUrl, + { sort: {}, filters: {} }, + filters + ); + return investigations[0] ?? null; +} + +/** + * Given either an investigation ID or an {@link Investigation} object, constructs a link to the {@link Investigation}. + * + * If providing an {@link Investigation} object, the {@link Investigation.investigationInstruments} field has to be present. + * + * @returns A URL to the investigation table, or `null` if the URL cannot be constructed due to missing info. + */ +async function buildInvestigationUrl({ + apiUrl, + facilityName, + investigation: providedInvestigation, + investigationId, +}: { + investigation?: Investigation; + investigationId?: Investigation['id']; + apiUrl: string; + facilityName: string; +}): Promise { + if (!investigationId && !providedInvestigation) { + // if neither an investigation object nor an investigation ID is provided, nothing can be built + // return nothing. + return null; + } + + if (facilityName !== 'ISIS' && facilityName !== 'DLS') { + if (investigationId) { + return `/browse/investigation/${investigationId}/dataset`; + } + if (providedInvestigation) { + return `/browse/investigation/${providedInvestigation.id}/dataset`; + } + return null; + } + + let investigation: Investigation | null; + if (providedInvestigation) { + investigation = providedInvestigation; + } else if (investigationId) { + investigation = await fetchInvestigation({ + apiUrl, + facilityName, + investigationId, + }); + } else { + return null; + } + + if (!investigation) return null; + + switch (facilityName) { + case 'ISIS': { + const instrument = + investigation?.investigationInstruments?.[0]?.instrument; + if (!instrument) return null; + + const facilityCycle = + investigation?.investigationFacilityCycles?.[0]?.facilityCycle; + if (!facilityCycle) return null; + + return `/browse/instrument/${instrument.id}/facilityCycle/${facilityCycle.id}/investigation/${investigation.id}/dataset`; + } + case 'DLS': + return `/browse/proposal/${investigation.name}/investigation/${investigation.id}/dataset`; + } +} + +export default buildInvestigationUrl; diff --git a/packages/datagateway-download/src/downloadCart/urlBuilders/index.ts b/packages/datagateway-download/src/downloadCart/urlBuilders/index.ts index 4479157f0..5e21bda0a 100644 --- a/packages/datagateway-download/src/downloadCart/urlBuilders/index.ts +++ b/packages/datagateway-download/src/downloadCart/urlBuilders/index.ts @@ -1,3 +1,3 @@ -export { default as buildInvestigationUrl } from './buildInvestigationUrl'; -export { default as buildDatasetUrl } from './buildDatasetUrl'; -export { default as buildDatafileUrl } from './buildDatafileUrl'; +export { default as buildInvestigationUrl } from './buildInvestigationUrl'; +export { default as buildDatasetUrl } from './buildDatasetUrl'; +export { default as buildDatafileUrl } from './buildDatafileUrl'; diff --git a/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx b/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx index 0954e24d8..38716bc52 100644 --- a/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx +++ b/packages/datagateway-download/src/downloadStatus/adminDownloadStatusTable.component.tsx @@ -72,74 +72,17 @@ const AdminDownloadStatusTable: React.FC = () => { // whether this is component's first render. const isFirstRender = useRef(true); - const buildQueryOffset = useCallback(() => { - let queryOffset = `WHERE download.facilityName = '${settings.facilityName}'`; - for (const [column, filter] of Object.entries(filters)) { - if (typeof filter === 'object') { - if (!Array.isArray(filter)) { - if ('startDate' in filter || 'endDate' in filter) { - const startDate = filter.startDate - ? `${filter.startDate}` - : '0001-01-01 00:00:00'; - const endDate = filter.endDate - ? `${filter.endDate}` - : '9999-12-31 23:59:00'; - - queryOffset += ` AND download.${column} BETWEEN {ts '${startDate}'} AND {ts '${endDate}'}`; - } - - if ('type' in filter && filter.type) { - // As UPPER is used need to pass text filters in upper case to avoid case sensitivity - // also need to escape single quotes - const filterValue = - typeof filter.value === 'string' - ? filter.type !== 'exact' - ? (filter.value as string).toUpperCase().replaceAll("'", "''") - : filter.value.replaceAll("'", "''") - : filter.value; - - // use switch statement to ensure TS can detect we cover all cases - switch (filter.type) { - case 'include': - queryOffset += ` AND UPPER(download.${column}) LIKE CONCAT('%', '${filterValue}', '%')`; - break; - case 'exclude': - queryOffset += ` AND UPPER(download.${column}) NOT LIKE CONCAT('%', '${filterValue}', '%')`; - break; - case 'exact': - queryOffset += ` AND download.${column} = '${filterValue}'`; - break; - default: { - const exhaustiveCheck: never = filter.type; - throw new Error( - `Unhandled text filter type: ${exhaustiveCheck}` - ); - } - } - } - } - } - } - - queryOffset += ' ORDER BY'; - for (const [column, order] of Object.entries(sort)) { - queryOffset += ` download.${column} ${order},`; - } - queryOffset += ' download.id ASC'; - - return queryOffset; - }, [filters, settings.facilityName, sort]); - const { data, - isLoading, + isPending, isFetched, isRefetching, fetchNextPage, refetch: refetchDownloads, dataUpdatedAt, } = useAdminDownloads({ - initialQueryOffset: `${buildQueryOffset()} LIMIT 0, 50`, + sort, + filters, }); const { data: accessMethods } = useDownloadTypes( @@ -148,19 +91,15 @@ const AdminDownloadStatusTable: React.FC = () => { ); const fetchMoreData = useCallback( - (offsetParams: IndexRange) => - fetchNextPage({ - pageParam: `${buildQueryOffset()} LIMIT ${offsetParams.startIndex}, ${ - offsetParams.stopIndex - offsetParams.startIndex + 1 - }`, - }), - [buildQueryOffset, fetchNextPage] + (_offsetParams: IndexRange) => fetchNextPage(), + [fetchNextPage] ); const refreshTable = useCallback(async () => { await Promise.all([ - // mark download progress queries as invalid so that react-query will refetch them as well. - queryClient.invalidateQueries([QueryKeys.DOWNLOAD_PROGRESS]), + queryClient.invalidateQueries({ + queryKey: [QueryKeys.DOWNLOAD_PROGRESS], + }), refetchDownloads(), ]); setRefreshDownloads(false); @@ -325,7 +264,7 @@ const AdminDownloadStatusTable: React.FC = () => { {/* Show loading progress if data is still being loaded */} - {(isLoading || isRefetching) && ( + {(isPending || isRefetching) && ( @@ -336,7 +275,7 @@ const AdminDownloadStatusTable: React.FC = () => { { } }} data={tableItems} - loading={isLoading} + loading={isPending} loadMoreRows={fetchMoreData} totalRowCount={Number.MAX_SAFE_INTEGER} actionsWidth={100} diff --git a/packages/datagateway-download/src/downloadStatus/downloadProgressIndicator.component.test.tsx b/packages/datagateway-download/src/downloadStatus/downloadProgressIndicator.component.test.tsx index a9fca1ac6..9ef04724b 100644 --- a/packages/datagateway-download/src/downloadStatus/downloadProgressIndicator.component.test.tsx +++ b/packages/datagateway-download/src/downloadStatus/downloadProgressIndicator.component.test.tsx @@ -15,12 +15,6 @@ const createTestQueryClient = (): QueryClient => retry: false, }, }, - // silence react-query errors - logger: { - log: console.log, - warn: console.warn, - error: vi.fn(), - }, }); const mockDownload: Download = { diff --git a/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx b/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx index 6013c51a6..67ab14700 100644 --- a/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx +++ b/packages/datagateway-download/src/downloadStatus/downloadStatusTable.component.tsx @@ -54,13 +54,11 @@ const DownloadStatusTable: React.FC = ( }>({}); const { data: downloads, - isLoading, + isPending, isFetched, refetch: refetchDownloads, dataUpdatedAt, - } = useDownloads({ - select: (data) => data.map(formatDownload), - }); + } = useDownloads((data) => data.map(formatDownload)); const { data: accessMethods } = useDownloadTypes( settings.facilityName, @@ -75,8 +73,9 @@ const DownloadStatusTable: React.FC = ( const refreshTable = useCallback(async () => { await Promise.all([ - // mark download progress queries as invalid so that react-query will refetch them as well. - queryClient.invalidateQueries([QueryKeys.DOWNLOAD_PROGRESS]), + queryClient.invalidateQueries({ + queryKey: [QueryKeys.DOWNLOAD_PROGRESS], + }), refetchDownloads(), ]); setRefreshTable(false); @@ -283,7 +282,7 @@ const DownloadStatusTable: React.FC = ( return ( {/* Show loading progress if data is still being loaded */} - {isLoading && ( + {isPending && ( @@ -294,7 +293,7 @@ const DownloadStatusTable: React.FC = ( = ( } }} data={sortedAndFilteredData} - loading={isLoading} + loading={isPending} // Pass in a custom actions column width to fit both buttons. actionsWidth={100} actions={[ @@ -423,7 +422,7 @@ const DownloadStatusTable: React.FC = ( function RemoveButton({ rowData, }: TableActionProps): JSX.Element { - const { isLoading: isDeleting, mutate: downloadDeleted } = + const { isPending: isDeleting, mutate: downloadDeleted } = useDownloadOrRestoreDownload(); const downloadItem = rowData as FormattedDownload; // const [isDeleting, setIsDeleting] = React.useState(false); diff --git a/packages/datagateway-download/src/settings.ts b/packages/datagateway-download/src/settings.ts index 820662174..1a3ef6837 100644 --- a/packages/datagateway-download/src/settings.ts +++ b/packages/datagateway-download/src/settings.ts @@ -1,8 +1,8 @@ -import { DownloadSettings } from './ConfigProvider'; - -export let settings: Promise; -export const setSettings = ( - newSettings: Promise -): void => { - settings = newSettings; -}; +import { DownloadSettings } from './ConfigProvider'; + +export let settings: Promise; +export const setSettings = ( + newSettings: Promise +): void => { + settings = newSettings; +}; diff --git a/packages/datagateway-download/src/setupTests.ts b/packages/datagateway-download/src/setupTests.ts index 7180b1352..f263d2c8d 100644 --- a/packages/datagateway-download/src/setupTests.ts +++ b/packages/datagateway-download/src/setupTests.ts @@ -1,43 +1,43 @@ - -import '@testing-library/jest-dom'; -import failOnConsole from 'vitest-fail-on-console'; - -failOnConsole(); - -vi.setConfig({ testTimeout: 20_000 }); - -// see https://github.com/testing-library/react-testing-library/issues/1197 -// and https://github.com/testing-library/user-event/issues/1115 -vi.stubGlobal('jest', { advanceTimersByTime: vi.advanceTimersByTime.bind(vi) }); - -// Add in ResizeObserver as it's not in jsdom's environment -vi.stubGlobal( - 'ResizeObserver', - vi.fn(() => ({ - observe: vi.fn(), - unobserve: vi.fn(), - disconnect: vi.fn(), - })) -); - -function noOp(): void { - // required as work-around for jsdom environment not implementing window.URL.createObjectURL method -} - -if (typeof window.URL.createObjectURL === 'undefined') { - Object.defineProperty(window.URL, 'createObjectURL', { value: noOp }); -} - -export const flushPromises = (): Promise => - new Promise((resolve) => setTimeout(resolve)); - -vi.mock('loglevel'); - -// Recreate jest behaviour by mocking with __mocks__ by mocking globally here -vi.mock('axios'); -vi.mock('react-i18next'); - -// Mock lodash.debounce to return the function we want to call. -vi.mock('lodash.debounce', () => ({ - default: (fn: (args: unknown) => unknown) => fn, -})); + +import '@testing-library/jest-dom'; +import failOnConsole from 'vitest-fail-on-console'; + +failOnConsole(); + +vi.setConfig({ testTimeout: 20_000 }); + +// see https://github.com/testing-library/react-testing-library/issues/1197 +// and https://github.com/testing-library/user-event/issues/1115 +vi.stubGlobal('jest', { advanceTimersByTime: vi.advanceTimersByTime.bind(vi) }); + +// Add in ResizeObserver as it's not in jsdom's environment +vi.stubGlobal( + 'ResizeObserver', + vi.fn(() => ({ + observe: vi.fn(), + unobserve: vi.fn(), + disconnect: vi.fn(), + })) +); + +function noOp(): void { + // required as work-around for jsdom environment not implementing window.URL.createObjectURL method +} + +if (typeof window.URL.createObjectURL === 'undefined') { + Object.defineProperty(window.URL, 'createObjectURL', { value: noOp }); +} + +export const flushPromises = (): Promise => + new Promise((resolve) => setTimeout(resolve)); + +vi.mock('loglevel'); + +// Recreate jest behaviour by mocking with __mocks__ by mocking globally here +vi.mock('axios'); +vi.mock('react-i18next'); + +// Mock lodash.debounce to return the function we want to call. +vi.mock('lodash.debounce', () => ({ + default: (fn: (args: unknown) => unknown) => fn, +})); diff --git a/packages/datagateway-download/src/testData.ts b/packages/datagateway-download/src/testData.ts index f49bf786a..4cd41dda0 100644 --- a/packages/datagateway-download/src/testData.ts +++ b/packages/datagateway-download/src/testData.ts @@ -1,348 +1,348 @@ -import { - Datafile, - Dataset, - Download, - DownloadCartItem, - FacilityCycle, - FormattedDownload, - Investigation, -} from 'datagateway-common'; -import type { DownloadSettings } from './ConfigProvider'; - -export const mockDownloadItems: Download[] = [ - { - createdAt: '2020-02-25T15:05:29Z', - downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], - email: 'test1@email.com', - facilityName: 'LILS', - fileName: 'test-file-1', - fullName: 'Person 1', - id: 1, - isDeleted: false, - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 1000, - status: 'COMPLETE', - transport: 'https', - userName: 'test user', - }, - { - createdAt: '2020-02-26T15:05:35Z', - downloadItems: [{ entityId: 2, entityType: 'investigation', id: 2 }], - email: 'test2@email.com', - facilityName: 'LILS', - fileName: 'test-file-2', - fullName: 'Person 2', - id: 2, - isDeleted: false, - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 2000, - status: 'PREPARING', - transport: 'globus', - userName: 'test user', - }, - { - createdAt: '2020-02-27T15:57:20Z', - downloadItems: [{ entityId: 3, entityType: 'investigation', id: 3 }], - email: 'test3@email.com', - facilityName: 'LILS', - fileName: 'test-file-3', - fullName: 'Person 3', - id: 3, - isDeleted: false, - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 3000, - status: 'RESTORING', - transport: 'https', - userName: 'test user', - }, - { - createdAt: '2020-02-28T15:57:28Z', - downloadItems: [{ entityId: 4, entityType: 'investigation', id: 4 }], - email: 'test4@email.com', - facilityName: 'LILS', - fileName: 'test-file-4', - fullName: 'Person 4', - id: 4, - isDeleted: true, - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 4000, - status: 'EXPIRED', - transport: 'globus', - userName: 'test user', - }, - { - createdAt: '2020-03-01T15:57:28Z[UTC]', - downloadItems: [{ entityId: 5, entityType: 'investigation', id: 5 }], - email: 'test5@email.com', - facilityName: 'LILS', - fileName: 'test-file-5', - fullName: 'Person 5', - id: 5, - isDeleted: false, - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 5000, - status: 'PAUSED', - transport: 'globus', - userName: 'test user', - }, -]; - -export const mockFormattedDownloadItems: FormattedDownload[] = [ - { - createdAt: '2020-02-25T15:05:29Z', - downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], - email: 'test1@email.com', - facilityName: 'LILS', - fileName: 'test-file-1', - fullName: 'Person 1', - id: 1, - isDeleted: false, - formattedIsDeleted: 'No', - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 1000, - status: 'COMPLETE', - formattedStatus: 'downloadStatus.complete', - transport: 'https', - userName: 'test user', - }, - { - createdAt: '2020-02-26T15:05:35Z', - downloadItems: [{ entityId: 2, entityType: 'investigation', id: 2 }], - email: 'test2@email.com', - facilityName: 'LILS', - fileName: 'test-file-2', - fullName: 'Person 2', - id: 2, - isDeleted: false, - formattedIsDeleted: 'No', - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 2000, - status: 'PREPARING', - formattedStatus: 'downloadStatus.preparing', - transport: 'globus', - userName: 'test user', - }, - { - createdAt: '2020-02-27T15:57:20Z', - downloadItems: [{ entityId: 3, entityType: 'investigation', id: 3 }], - email: 'test3@email.com', - facilityName: 'LILS', - fileName: 'test-file-3', - fullName: 'Person 3', - id: 3, - isDeleted: false, - formattedIsDeleted: 'No', - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 3000, - status: 'RESTORING', - formattedStatus: 'downloadStatus.restoring', - transport: 'https', - userName: 'test user', - }, - { - createdAt: '2020-02-28T15:57:28Z', - downloadItems: [{ entityId: 4, entityType: 'investigation', id: 4 }], - email: 'test4@email.com', - facilityName: 'LILS', - fileName: 'test-file-4', - fullName: 'Person 4', - id: 4, - isDeleted: true, - formattedIsDeleted: 'Yes', - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 4000, - status: 'EXPIRED', - formattedStatus: 'downloadStatus.expired', - transport: 'globus', - userName: 'test user', - }, - { - createdAt: '2020-03-01T15:57:28Z[UTC]', - downloadItems: [{ entityId: 5, entityType: 'investigation', id: 5 }], - email: 'test5@email.com', - facilityName: 'LILS', - fileName: 'test-file-5', - fullName: 'Person 5', - id: 5, - isDeleted: false, - formattedIsDeleted: 'No', - isEmailSent: true, - isTwoLevel: false, - preparedId: 'test-prepared-id', - sessionId: 'test-session-id', - size: 5000, - status: 'PAUSED', - formattedStatus: 'downloadStatus.paused', - transport: 'globus', - userName: 'test user', - }, -]; - -export const mockCartItems: DownloadCartItem[] = [ - { - entityId: 1, - entityType: 'investigation', - id: 1, - name: 'INVESTIGATION 1', - parentEntities: [], - }, - { - entityId: 2, - entityType: 'investigation', - id: 2, - name: 'INVESTIGATION 2', - parentEntities: [], - }, - { - entityId: 3, - entityType: 'dataset', - id: 3, - name: 'DATASET 1', - parentEntities: [], - }, - { - entityId: 4, - entityType: 'datafile', - id: 4, - name: 'DATAFILE 1', - parentEntities: [], - }, -]; - -export const mockFacilityCycles: FacilityCycle[] = [ - { - id: 12938, - name: 'studio toughness', - description: 'He decided water-skiing on a frozen lake wasn’t a good idea.', - startDate: '2006-01-20T16:30:17Z', - endDate: '2007-01-20T16:30:17Z', - }, - { - id: 402, - name: 'within cell interlinked', - description: 'He waited for the stop sign to turn to a go sign.', - startDate: '2017-03-17T14:03:11Z', - endDate: '2020-11-29T05:41:54Z', - }, -]; - -export const mockInvestigations: Investigation[] = [ - { - id: 58, - title: 'Happiness can be found in the depths of chocolate pudding.', - name: 'investigation news', - visitId: 'CqJN', - startDate: '2018-03-09T08:19:55Z', - endDate: '2018-03-29T08:19:55Z', - investigationInstruments: [ - { - id: 446, - instrument: { - id: 937, - name: 'instrument fame', - }, - }, - ], - investigationFacilityCycles: [ - { - id: 446, - facilityCycle: mockFacilityCycles[1], - }, - ], - }, - { - id: 993, - title: 'I ate a sock because people on the Internet told me to.', - name: 'investigation inn', - visitId: 'z0bLi1f3', - startDate: '2019-08-08T22:27:07Z', - endDate: '2019-09-08T22:27:07Z', - investigationInstruments: [ - { - id: 262, - instrument: { - id: 927, - name: 'instrument case', - }, - }, - ], - investigationFacilityCycles: [ - { - id: 262, - facilityCycle: mockFacilityCycles[1], - }, - ], - }, -]; - -export const mockDatasets: Dataset[] = [ - { - id: 856, - name: 'dataset modern', - investigation: mockInvestigations[0], - createTime: '2018-03-10T08:19:55Z', - modTime: '2018-03-10T09:19:55Z', - }, - { - id: 535, - name: 'dataset lazy', - investigation: mockInvestigations[1], - createTime: '2019-09-01T22:27:07Z', - modTime: '2019-09-02T22:27:07Z', - }, -]; - -export const mockDatafiles: Datafile[] = [ - { - id: 70, - name: 'datafile weekend', - modTime: '2018-03-10T08:19:55Z', - createTime: '2018-03-10T08:19:55Z', - dataset: mockDatasets[0], - }, -]; - -// Create our mocked datagateway-download settings file. -export const mockedSettings: DownloadSettings = { - facilityName: 'LILS', - apiUrl: 'https://example.com/api', - downloadApiUrl: 'https://example.com/downloadApi', - idsUrl: 'https://example.com/ids', - doiMinterUrl: 'https://example.com/doiMinter', - dataCiteUrl: 'https://example.com/dataCite', - bioportalUrl: 'https://example.com/bioPortal', - fileCountMax: 5000, - totalSizeMax: 1000000000000, - uiFeatures: { - downloadProgress: false, - }, - routes: [], - helpSteps: [], -}; +import { + Datafile, + Dataset, + Download, + DownloadCartItem, + FacilityCycle, + FormattedDownload, + Investigation, +} from 'datagateway-common'; +import type { DownloadSettings } from './ConfigProvider'; + +export const mockDownloadItems: Download[] = [ + { + createdAt: '2020-02-25T15:05:29Z', + downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], + email: 'test1@email.com', + facilityName: 'LILS', + fileName: 'test-file-1', + fullName: 'Person 1', + id: 1, + isDeleted: false, + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 1000, + status: 'COMPLETE', + transport: 'https', + userName: 'test user', + }, + { + createdAt: '2020-02-26T15:05:35Z', + downloadItems: [{ entityId: 2, entityType: 'investigation', id: 2 }], + email: 'test2@email.com', + facilityName: 'LILS', + fileName: 'test-file-2', + fullName: 'Person 2', + id: 2, + isDeleted: false, + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 2000, + status: 'PREPARING', + transport: 'globus', + userName: 'test user', + }, + { + createdAt: '2020-02-27T15:57:20Z', + downloadItems: [{ entityId: 3, entityType: 'investigation', id: 3 }], + email: 'test3@email.com', + facilityName: 'LILS', + fileName: 'test-file-3', + fullName: 'Person 3', + id: 3, + isDeleted: false, + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 3000, + status: 'RESTORING', + transport: 'https', + userName: 'test user', + }, + { + createdAt: '2020-02-28T15:57:28Z', + downloadItems: [{ entityId: 4, entityType: 'investigation', id: 4 }], + email: 'test4@email.com', + facilityName: 'LILS', + fileName: 'test-file-4', + fullName: 'Person 4', + id: 4, + isDeleted: true, + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 4000, + status: 'EXPIRED', + transport: 'globus', + userName: 'test user', + }, + { + createdAt: '2020-03-01T15:57:28Z[UTC]', + downloadItems: [{ entityId: 5, entityType: 'investigation', id: 5 }], + email: 'test5@email.com', + facilityName: 'LILS', + fileName: 'test-file-5', + fullName: 'Person 5', + id: 5, + isDeleted: false, + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 5000, + status: 'PAUSED', + transport: 'globus', + userName: 'test user', + }, +]; + +export const mockFormattedDownloadItems: FormattedDownload[] = [ + { + createdAt: '2020-02-25T15:05:29Z', + downloadItems: [{ entityId: 1, entityType: 'investigation', id: 1 }], + email: 'test1@email.com', + facilityName: 'LILS', + fileName: 'test-file-1', + fullName: 'Person 1', + id: 1, + isDeleted: false, + formattedIsDeleted: 'No', + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 1000, + status: 'COMPLETE', + formattedStatus: 'downloadStatus.complete', + transport: 'https', + userName: 'test user', + }, + { + createdAt: '2020-02-26T15:05:35Z', + downloadItems: [{ entityId: 2, entityType: 'investigation', id: 2 }], + email: 'test2@email.com', + facilityName: 'LILS', + fileName: 'test-file-2', + fullName: 'Person 2', + id: 2, + isDeleted: false, + formattedIsDeleted: 'No', + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 2000, + status: 'PREPARING', + formattedStatus: 'downloadStatus.preparing', + transport: 'globus', + userName: 'test user', + }, + { + createdAt: '2020-02-27T15:57:20Z', + downloadItems: [{ entityId: 3, entityType: 'investigation', id: 3 }], + email: 'test3@email.com', + facilityName: 'LILS', + fileName: 'test-file-3', + fullName: 'Person 3', + id: 3, + isDeleted: false, + formattedIsDeleted: 'No', + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 3000, + status: 'RESTORING', + formattedStatus: 'downloadStatus.restoring', + transport: 'https', + userName: 'test user', + }, + { + createdAt: '2020-02-28T15:57:28Z', + downloadItems: [{ entityId: 4, entityType: 'investigation', id: 4 }], + email: 'test4@email.com', + facilityName: 'LILS', + fileName: 'test-file-4', + fullName: 'Person 4', + id: 4, + isDeleted: true, + formattedIsDeleted: 'Yes', + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 4000, + status: 'EXPIRED', + formattedStatus: 'downloadStatus.expired', + transport: 'globus', + userName: 'test user', + }, + { + createdAt: '2020-03-01T15:57:28Z[UTC]', + downloadItems: [{ entityId: 5, entityType: 'investigation', id: 5 }], + email: 'test5@email.com', + facilityName: 'LILS', + fileName: 'test-file-5', + fullName: 'Person 5', + id: 5, + isDeleted: false, + formattedIsDeleted: 'No', + isEmailSent: true, + isTwoLevel: false, + preparedId: 'test-prepared-id', + sessionId: 'test-session-id', + size: 5000, + status: 'PAUSED', + formattedStatus: 'downloadStatus.paused', + transport: 'globus', + userName: 'test user', + }, +]; + +export const mockCartItems: DownloadCartItem[] = [ + { + entityId: 1, + entityType: 'investigation', + id: 1, + name: 'INVESTIGATION 1', + parentEntities: [], + }, + { + entityId: 2, + entityType: 'investigation', + id: 2, + name: 'INVESTIGATION 2', + parentEntities: [], + }, + { + entityId: 3, + entityType: 'dataset', + id: 3, + name: 'DATASET 1', + parentEntities: [], + }, + { + entityId: 4, + entityType: 'datafile', + id: 4, + name: 'DATAFILE 1', + parentEntities: [], + }, +]; + +export const mockFacilityCycles: FacilityCycle[] = [ + { + id: 12938, + name: 'studio toughness', + description: 'He decided water-skiing on a frozen lake wasn’t a good idea.', + startDate: '2006-01-20T16:30:17Z', + endDate: '2007-01-20T16:30:17Z', + }, + { + id: 402, + name: 'within cell interlinked', + description: 'He waited for the stop sign to turn to a go sign.', + startDate: '2017-03-17T14:03:11Z', + endDate: '2020-11-29T05:41:54Z', + }, +]; + +export const mockInvestigations: Investigation[] = [ + { + id: 58, + title: 'Happiness can be found in the depths of chocolate pudding.', + name: 'investigation news', + visitId: 'CqJN', + startDate: '2018-03-09T08:19:55Z', + endDate: '2018-03-29T08:19:55Z', + investigationInstruments: [ + { + id: 446, + instrument: { + id: 937, + name: 'instrument fame', + }, + }, + ], + investigationFacilityCycles: [ + { + id: 446, + facilityCycle: mockFacilityCycles[1], + }, + ], + }, + { + id: 993, + title: 'I ate a sock because people on the Internet told me to.', + name: 'investigation inn', + visitId: 'z0bLi1f3', + startDate: '2019-08-08T22:27:07Z', + endDate: '2019-09-08T22:27:07Z', + investigationInstruments: [ + { + id: 262, + instrument: { + id: 927, + name: 'instrument case', + }, + }, + ], + investigationFacilityCycles: [ + { + id: 262, + facilityCycle: mockFacilityCycles[1], + }, + ], + }, +]; + +export const mockDatasets: Dataset[] = [ + { + id: 856, + name: 'dataset modern', + investigation: mockInvestigations[0], + createTime: '2018-03-10T08:19:55Z', + modTime: '2018-03-10T09:19:55Z', + }, + { + id: 535, + name: 'dataset lazy', + investigation: mockInvestigations[1], + createTime: '2019-09-01T22:27:07Z', + modTime: '2019-09-02T22:27:07Z', + }, +]; + +export const mockDatafiles: Datafile[] = [ + { + id: 70, + name: 'datafile weekend', + modTime: '2018-03-10T08:19:55Z', + createTime: '2018-03-10T08:19:55Z', + dataset: mockDatasets[0], + }, +]; + +// Create our mocked datagateway-download settings file. +export const mockedSettings: DownloadSettings = { + facilityName: 'LILS', + apiUrl: 'https://example.com/api', + downloadApiUrl: 'https://example.com/downloadApi', + idsUrl: 'https://example.com/ids', + doiMinterUrl: 'https://example.com/doiMinter', + dataCiteUrl: 'https://example.com/dataCite', + bioportalUrl: 'https://example.com/bioPortal', + fileCountMax: 5000, + totalSizeMax: 1000000000000, + uiFeatures: { + downloadProgress: false, + }, + routes: [], + helpSteps: [], +}; diff --git a/packages/datagateway-search/package.json b/packages/datagateway-search/package.json index 65f2ed99d..81a094706 100644 --- a/packages/datagateway-search/package.json +++ b/packages/datagateway-search/package.json @@ -10,8 +10,8 @@ "@mui/icons-material": "5.18.0", "@mui/material": "5.18.0", "@mui/x-date-pickers": "6.20.2", - "@tanstack/react-query": "4.43.0", - "@tanstack/react-query-devtools": "4.43.0", + "@tanstack/react-query": "5.90.21", + "@tanstack/react-query-devtools": "5.91.3", "@types/history": "4.7.11", "@types/jsrsasign": "10.5.2", "@types/lodash.isequal": "4.5.8", @@ -80,6 +80,7 @@ ] }, "devDependencies": { + "@tanstack/eslint-plugin-query": "5.91.4", "@testing-library/cypress": "10.1.0", "@testing-library/dom": "10.4.1", "@testing-library/jest-dom": "6.9.1", diff --git a/packages/datagateway-search/src/App.tsx b/packages/datagateway-search/src/App.tsx index ec812a380..834600023 100644 --- a/packages/datagateway-search/src/App.tsx +++ b/packages/datagateway-search/src/App.tsx @@ -1,16 +1,25 @@ import { + QueryCache, + QueryClient, + QueryClientProvider, +} from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { + BroadcastSignOutType, DGCommonMiddleware, DGThemeProvider, MicroFrontendId, Preloader, - BroadcastSignOutType, - RequestPluginRerenderType, QueryClientSettingsUpdaterRedux, + RequestPluginRerenderType, + queryCacheConfig, } from 'datagateway-common'; import log from 'loglevel'; import React from 'react'; -import { connect, Provider } from 'react-redux'; -import { AnyAction, applyMiddleware, compose, createStore, Store } from 'redux'; +import { Translation } from 'react-i18next'; +import { Provider, connect } from 'react-redux'; +import { BrowserRouter } from 'react-router-dom'; +import { AnyAction, Store, applyMiddleware, compose, createStore } from 'redux'; import { createLogger } from 'redux-logger'; import thunk, { ThunkDispatch } from 'redux-thunk'; import './App.css'; @@ -18,10 +27,6 @@ import SearchPageContainer from './searchPageContainer.component'; import { configureApp } from './state/actions'; import { StateType } from './state/app.types'; import AppReducer from './state/reducers/app.reducer'; -import { Translation } from 'react-i18next'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; -import { BrowserRouter } from 'react-router-dom'; /* eslint-disable @typescript-eslint/no-explicit-any */ const composeEnhancers = @@ -51,6 +56,7 @@ const queryClient = new QueryClient({ staleTime: 300000, }, }, + queryCache: new QueryCache(queryCacheConfig), }); document.addEventListener(MicroFrontendId, (e) => { diff --git a/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx b/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx index c40627631..063abcc54 100644 --- a/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx +++ b/packages/datagateway-search/src/card/datasetSearchCardView.component.test.tsx @@ -135,14 +135,6 @@ describe('Dataset - Card View', () => { // wait for queries to finish fetching await waitFor(() => !queryClient.isFetching()); - expect( - queryClient.getQueryState(['search', 'Dataset'], { exact: false })?.status - ).toBe('loading'); - expect( - queryClient.getQueryState(['search', 'Dataset'], { exact: false }) - ?.fetchStatus - ).toBe('idle'); - expect(screen.queryAllByTestId('card')).toHaveLength(0); }); diff --git a/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx b/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx index 96957a188..42090ee3d 100644 --- a/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx +++ b/packages/datagateway-search/src/card/datasetSearchCardView.component.tsx @@ -82,7 +82,7 @@ const DatasetCardView: React.FC = (props) => { (state: StateType) => state.dgsearch.maxNumResults ); - const { data, isLoading, isFetching, hasNextPage, fetchNextPage, refetch } = + const { data, isPending, isFetching, hasNextPage, fetchNextPage, refetch } = useLuceneSearchInfinite( 'Dataset', { @@ -112,32 +112,34 @@ const DatasetCardView: React.FC = (props) => { // facet since the number is confusing for datafiles select: (data) => ({ ...data, - pages: data.pages.map((searchResponse) => ({ - ...searchResponse, - dimensions: { - ...searchResponse.dimensions, - ...(searchResponse.dimensions?.[ - 'InvestigationInstrument.instrument.name' - ] - ? { - 'InvestigationInstrument.instrument.name': Object.keys( - searchResponse.dimensions?.[ - 'InvestigationInstrument.instrument.name' - ] - ).reduce( - ( - accumulator: { [key: string]: undefined }, - current: string - ) => { - accumulator[current] = undefined; - return accumulator; - }, - {} - ), - } - : {}), - }, - })), + pages: data.pages.map( + (searchResponse): SearchResponse => ({ + ...searchResponse, + dimensions: { + ...searchResponse.dimensions, + ...(searchResponse.dimensions?.[ + 'InvestigationInstrument.instrument.name' + ] + ? { + 'InvestigationInstrument.instrument.name': Object.keys( + searchResponse.dimensions?.[ + 'InvestigationInstrument.instrument.name' + ] + ).reduce( + ( + accumulator: { [key: string]: undefined }, + current: string + ) => { + accumulator[current] = undefined; + return accumulator; + }, + {} + ), + } + : {}), + }, + }) + ), }), } ); @@ -457,8 +459,8 @@ const DatasetCardView: React.FC = (props) => { onFilter={pushFilter} onSort={handleSort} onResultsChange={pushResults} - loadedData={!isLoading} - loadedCount={!isLoading} + loadedData={!isPending} + loadedCount={!isPending} filters={{}} sort={{}} page={page} diff --git a/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx b/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx index 7bf163205..1d7682edd 100644 --- a/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx +++ b/packages/datagateway-search/src/card/investigationSearchCardView.component.test.tsx @@ -139,15 +139,6 @@ describe('Investigation - Card View', () => { // wait for queries to finish fetching await waitFor(() => !queryClient.isFetching()); - expect( - queryClient.getQueryState(['search', 'Investigation'], { exact: false }) - ?.status - ).toBe('loading'); - expect( - queryClient.getQueryState(['search', 'Investigation'], { exact: false }) - ?.fetchStatus - ).toBe('idle'); - expect(screen.queryAllByTestId('card')).toHaveLength(0); }); diff --git a/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx b/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx index 18de38bdd..56d10a7b9 100644 --- a/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx +++ b/packages/datagateway-search/src/card/investigationSearchCardView.component.tsx @@ -94,7 +94,7 @@ const InvestigationCardView: React.FC = (props) => { (state: StateType) => state.dgsearch.maxNumResults ); - const { data, isLoading, isFetching, hasNextPage, fetchNextPage, refetch } = + const { data, isPending, isFetching, hasNextPage, fetchNextPage, refetch } = useLuceneSearchInfinite( 'Investigation', { @@ -433,8 +433,8 @@ const InvestigationCardView: React.FC = (props) => { onFilter={pushFilter} onSort={handleSort} onResultsChange={pushResults} - loadedData={!isLoading} - loadedCount={!isLoading} + loadedData={!isPending} + loadedCount={!isPending} filters={{}} sort={sort} page={page} diff --git a/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterDateTimeSelector.component.tsx b/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterDateTimeSelector.component.tsx index 1e844b67c..eb79e1960 100644 --- a/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterDateTimeSelector.component.tsx +++ b/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterDateTimeSelector.component.tsx @@ -68,16 +68,14 @@ function ParameterDateTimeSelector({ ]; }, [currentDate, entityName]); - const { data: facets, isLoading: isLoadingFacets } = useLuceneFacet( + const { data: facets, isPending: isLoadingFacets } = useLuceneFacet( entityName, facetRequests, { [`${entityName.toLowerCase()}.id`]: allIds, 'type.name': parameterName, } as FiltersType, - { - select: parameterFacetsFromSearchResponse, - } + parameterFacetsFromSearchResponse ); const [selectedFacet, setSelectedFacet] = diff --git a/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterFacetList.component.tsx b/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterFacetList.component.tsx index 6930fa818..05dc752f3 100644 --- a/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterFacetList.component.tsx +++ b/packages/datagateway-search/src/facet/components/facetPanel/parameterFilters/valueSelectors/parameterFacetList.component.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { CircularProgress, ListItemText, @@ -8,15 +7,16 @@ import { Typography, } from '@mui/material'; import { + useLuceneFacet, type FacetRequest, type FiltersType, - useLuceneFacet, } from 'datagateway-common'; -import type { ParameterValueFacet } from '../parameterFilterTypes'; -import parameterFacetsFromSearchResponse from '../parameterFacetsFromSearchResponse'; -import ParameterValueSelectorProps from './parameterValueSelectorProps'; +import React from 'react'; import { useTranslation } from 'react-i18next'; import { ExtraSmallChip } from '../../toggleableFilterItem.component'; +import parameterFacetsFromSearchResponse from '../parameterFacetsFromSearchResponse'; +import type { ParameterValueFacet } from '../parameterFilterTypes'; +import ParameterValueSelectorProps from './parameterValueSelectorProps'; function ParameterFacetList({ entityName, @@ -33,16 +33,14 @@ function ParameterFacetList({ }, ]; - const { data: facets, isLoading: isLoadingFacets } = useLuceneFacet( + const { data: facets, isPending: isLoadingFacets } = useLuceneFacet( entityName, facetRequests, { [`${entityName.toLowerCase()}.id`]: allIds, 'type.name': parameterName, } as FiltersType, - { - select: parameterFacetsFromSearchResponse, - } + parameterFacetsFromSearchResponse ); const [selectedFacet, setSelectedFacet] = diff --git a/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx b/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx index 1ddd3c00c..71de5fa0e 100644 --- a/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx +++ b/packages/datagateway-search/src/table/datafileSearchTable.component.test.tsx @@ -237,15 +237,6 @@ describe('Datafile search table component', () => { // wait for queries to finish fetching await waitFor(() => !queryClient.isFetching()); - expect( - queryClient.getQueryState(['search', 'Datafile'], { exact: false }) - ?.status - ).toBe('loading'); - expect( - queryClient.getQueryState(['search', 'Datafile'], { exact: false }) - ?.fetchStatus - ).toBe('idle'); - expect(queryAllRows()).toHaveLength(0); }); diff --git a/packages/datagateway-search/src/table/datafileSearchTable.component.tsx b/packages/datagateway-search/src/table/datafileSearchTable.component.tsx index d244a0695..2ea1199ea 100644 --- a/packages/datagateway-search/src/table/datafileSearchTable.component.tsx +++ b/packages/datagateway-search/src/table/datafileSearchTable.component.tsx @@ -92,41 +92,43 @@ const DatafileSearchTable: React.FC = (props) => { // facet since the number is confusing for datafiles select: (data) => ({ ...data, - pages: data.pages.map((searchResponse) => ({ - ...searchResponse, - dimensions: { - ...searchResponse.dimensions, - ...(searchResponse.dimensions?.[ - 'InvestigationInstrument.instrument.name' - ] - ? { - 'InvestigationInstrument.instrument.name': Object.keys( - searchResponse.dimensions?.[ - 'InvestigationInstrument.instrument.name' - ] - ).reduce( - ( - accumulator: { [key: string]: undefined }, - current: string - ) => { - accumulator[current] = undefined; - return accumulator; - }, - {} - ), - } - : {}), - }, - })), + pages: data.pages.map( + (searchResponse): SearchResponse => ({ + ...searchResponse, + dimensions: { + ...searchResponse.dimensions, + ...(searchResponse.dimensions?.[ + 'InvestigationInstrument.instrument.name' + ] + ? { + 'InvestigationInstrument.instrument.name': Object.keys( + searchResponse.dimensions?.[ + 'InvestigationInstrument.instrument.name' + ] + ).reduce( + ( + accumulator: { [key: string]: undefined }, + current: string + ) => { + accumulator[current] = undefined; + return accumulator; + }, + {} + ), + } + : {}), + }, + }) + ), }), } ); const [t] = useTranslation(); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('datafile'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('datafile'); useSearchResultCounter({ diff --git a/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx b/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx index 55e909998..9f2b2e1fa 100644 --- a/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx +++ b/packages/datagateway-search/src/table/datasetSearchTable.component.test.tsx @@ -211,14 +211,6 @@ describe('Dataset table component', () => { // wait for queries to finish fetching await waitFor(() => !queryClient.isFetching()); - expect( - queryClient.getQueryState(['search', 'Dataset'], { exact: false })?.status - ).toBe('loading'); - expect( - queryClient.getQueryState(['search', 'Dataset'], { exact: false }) - ?.fetchStatus - ).toBe('idle'); - expect(queryAllRows()).toHaveLength(0); }); diff --git a/packages/datagateway-search/src/table/datasetSearchTable.component.tsx b/packages/datagateway-search/src/table/datasetSearchTable.component.tsx index 6e6fd9c18..f05d2c780 100644 --- a/packages/datagateway-search/src/table/datasetSearchTable.component.tsx +++ b/packages/datagateway-search/src/table/datasetSearchTable.component.tsx @@ -100,42 +100,44 @@ const DatasetSearchTable: React.FC = ({ hierarchy }) => { // facet since the number is confusing for datafiles select: (data) => ({ ...data, - pages: data.pages.map((searchResponse) => ({ - ...searchResponse, - dimensions: { - ...searchResponse.dimensions, - ...(searchResponse.dimensions?.[ - 'InvestigationInstrument.instrument.name' - ] - ? { - 'InvestigationInstrument.instrument.name': Object.keys( - searchResponse.dimensions?.[ - 'InvestigationInstrument.instrument.name' - ] - ).reduce( - ( - accumulator: { [key: string]: undefined }, - current: string - ) => { - accumulator[current] = undefined; - return accumulator; - }, - {} - ), - } - : {}), - }, - })), + pages: data.pages.map( + (searchResponse): SearchResponse => ({ + ...searchResponse, + dimensions: { + ...searchResponse.dimensions, + ...(searchResponse.dimensions?.[ + 'InvestigationInstrument.instrument.name' + ] + ? { + 'InvestigationInstrument.instrument.name': Object.keys( + searchResponse.dimensions?.[ + 'InvestigationInstrument.instrument.name' + ] + ).reduce( + ( + accumulator: { [key: string]: undefined }, + current: string + ) => { + accumulator[current] = undefined; + return accumulator; + }, + {} + ), + } + : {}), + }, + }) + ), }), } ); const [t] = useTranslation(); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('dataset'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('dataset'); const { diff --git a/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx b/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx index 4263f5deb..5b26e5837 100644 --- a/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx +++ b/packages/datagateway-search/src/table/investigationSearchTable.component.test.tsx @@ -32,16 +32,6 @@ import { initialState } from '../state/reducers/dgsearch.reducer'; import { mockInvestigation } from '../testData'; import InvestigationSearchTable from './investigationSearchTable.component'; -vi.mock('datagateway-common', async () => { - const originalModule = await vi.importActual('datagateway-common'); - - return { - __esModule: true, - ...originalModule, - handleICATError: vi.fn(), - }; -}); - describe('Investigation Search Table component', () => { const mockStore = configureStore([thunk]); let container: HTMLDivElement; @@ -238,15 +228,6 @@ describe('Investigation Search Table component', () => { // wait for queries to finish fetching await waitFor(() => !queryClient.isFetching()); - expect( - queryClient.getQueryState(['search', 'Investigation'], { exact: false }) - ?.status - ).toBe('loading'); - expect( - queryClient.getQueryState(['search', 'Investigation'], { exact: false }) - ?.fetchStatus - ).toBe('idle'); - expect(queryAllRows()).toHaveLength(0); }); diff --git a/packages/datagateway-search/src/table/investigationSearchTable.component.tsx b/packages/datagateway-search/src/table/investigationSearchTable.component.tsx index d459e79a4..f4ad26bb2 100644 --- a/packages/datagateway-search/src/table/investigationSearchTable.component.tsx +++ b/packages/datagateway-search/src/table/investigationSearchTable.component.tsx @@ -102,10 +102,10 @@ const InvestigationSearchTable: React.FC = (props) => { { enabled: investigation } ); - const { data: cartItems, isLoading: cartLoading } = useCart(); - const { mutate: addToCart, isLoading: addToCartLoading } = + const { data: cartItems, isPending: cartLoading } = useCart(); + const { mutate: addToCart, isPending: addToCartLoading } = useAddToCart('investigation'); - const { mutate: removeFromCart, isLoading: removeFromCartLoading } = + const { mutate: removeFromCart, isPending: removeFromCartLoading } = useRemoveFromCart('investigation'); const { diff --git a/yarn.lock b/yarn.lock index a9ece52b7..6d6fc704f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1711,53 +1711,55 @@ __metadata: languageName: node linkType: hard -"@tanstack/match-sorter-utils@npm:^8.7.0": - version: 8.19.4 - resolution: "@tanstack/match-sorter-utils@npm:8.19.4" +"@tanstack/eslint-plugin-query@npm:5.91.4": + version: 5.91.4 + resolution: "@tanstack/eslint-plugin-query@npm:5.91.4" dependencies: - remove-accents: 0.5.0 - checksum: 7ec302d75be1f65a3e91dd30b567e47e56c13577937276ef3a3ac7c8560a0b9f6dcaca44dd877189375ce9e7401d250895265adb0741ec867eda4e47d0f07dc0 + "@typescript-eslint/utils": ^8.48.0 + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + typescript: ^5.0.0 + peerDependenciesMeta: + typescript: + optional: true + checksum: 6bfcba27bbc2541e85e499d9e728603ad1b8f7b1234f6c9252e2cb6d3dcfb394f1ab542f32a62727722934734da9145c116e4ad5057984642fbcac0a4218dfc5 languageName: node linkType: hard -"@tanstack/query-core@npm:4.43.0": - version: 4.43.0 - resolution: "@tanstack/query-core@npm:4.43.0" - checksum: 3e37f8cedd533aaacaaa40bb940b45c4c428e48f11db15665932c71d15d200c0f69ae3ca2bb3233078f1620f521712a4294610b5b87ffe51cd07acffacaef17d +"@tanstack/query-core@npm:5.90.20": + version: 5.90.20 + resolution: "@tanstack/query-core@npm:5.90.20" + checksum: 47a94b3b8f9d68db6622183cbb2c0e7e79f2e71f4664e3b7b51f24fb77800188d919b5f35f522107518a2bcf7b345948e51b24c86e47c62fbaaa9788a9c61b40 languageName: node linkType: hard -"@tanstack/react-query-devtools@npm:4.43.0": - version: 4.43.0 - resolution: "@tanstack/react-query-devtools@npm:4.43.0" +"@tanstack/query-devtools@npm:5.93.0": + version: 5.93.0 + resolution: "@tanstack/query-devtools@npm:5.93.0" + checksum: b7fbbb6f87d7c73e8fc767108ebb753de15302b428e451a43ba6c4c16e535f0ac1102acb8328667c94464ed18dc369ea30ad21234435cbf6528f48f4d22e51a3 + languageName: node + linkType: hard + +"@tanstack/react-query-devtools@npm:5.91.3": + version: 5.91.3 + resolution: "@tanstack/react-query-devtools@npm:5.91.3" dependencies: - "@tanstack/match-sorter-utils": ^8.7.0 - superjson: ^1.10.0 - use-sync-external-store: ^1.2.0 + "@tanstack/query-devtools": 5.93.0 peerDependencies: - "@tanstack/react-query": ^4.43.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: debd8accde638388164936f1c0878edfc62720ae79aff966840adc40a38011db1dae0af82784904d367187b7f5b82d9899e5c84ca5ebc87e8adf5aef76fb11bd + "@tanstack/react-query": ^5.90.20 + react: ^18 || ^19 + checksum: d48fbfdc76e30e652bb80aaad9f1cceec473242e7a8c6d4070bf689cfa9a6e10488cd1cadaaa558dba60cbf78059f4ee4759a626c8daaf713c21d7288430a0cd languageName: node linkType: hard -"@tanstack/react-query@npm:4.43.0": - version: 4.43.0 - resolution: "@tanstack/react-query@npm:4.43.0" +"@tanstack/react-query@npm:5.90.21": + version: 5.90.21 + resolution: "@tanstack/react-query@npm:5.90.21" dependencies: - "@tanstack/query-core": 4.43.0 - use-sync-external-store: ^1.6.0 + "@tanstack/query-core": 5.90.20 peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - react-native: "*" - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true - checksum: 059d4c0663b8f9b8ce592f22bf7de7121da4861bff5d5c9cfc9f527251243cd983d8377dcb7267d080282e5b0ff950790d1cca83c5ce4406be4bb9371b4263ec + react: ^18 || ^19 + checksum: 229c693d0c19486042ec8bc4e584d3a434bde41aa33918f38c2aedc045b2f5cb11e02f48d9322b3c3af42d432906542719407a82e382d832bbf2eeb2099eccff languageName: node linkType: hard @@ -2243,7 +2245,7 @@ __metadata: languageName: node linkType: hard -"@typescript-eslint/utils@npm:8.56.1, @typescript-eslint/utils@npm:^8.56.0": +"@typescript-eslint/utils@npm:8.56.1, @typescript-eslint/utils@npm:^8.48.0, @typescript-eslint/utils@npm:^8.56.0": version: 8.56.1 resolution: "@typescript-eslint/utils@npm:8.56.1" dependencies: @@ -3354,15 +3356,6 @@ __metadata: languageName: node linkType: hard -"copy-anything@npm:^3.0.2": - version: 3.0.5 - resolution: "copy-anything@npm:3.0.5" - dependencies: - is-what: ^4.1.8 - checksum: d39f6601c16b7cbd81cdb1c1f40f2bf0f2ca0297601cf7bfbb4ef1d85374a6a89c559502329f5bada36604464df17623e111fe19a9bb0c3f6b1c92fe2cbe972f - languageName: node - linkType: hard - "core-util-is@npm:1.0.2": version: 1.0.2 resolution: "core-util-is@npm:1.0.2" @@ -3563,8 +3556,9 @@ __metadata: "@mui/lab": 5.0.0-alpha.177 "@mui/material": 5.18.0 "@mui/x-date-pickers": 6.20.2 - "@tanstack/react-query": 4.43.0 - "@tanstack/react-query-devtools": 4.43.0 + "@tanstack/eslint-plugin-query": 5.91.4 + "@tanstack/react-query": 5.90.21 + "@tanstack/react-query-devtools": 5.91.3 "@testing-library/dom": 10.4.1 "@testing-library/jest-dom": 6.9.1 "@testing-library/react": 16.3.2 @@ -3631,8 +3625,9 @@ __metadata: "@emotion/styled": 11.14.1 "@mui/icons-material": 5.18.0 "@mui/material": 5.18.0 - "@tanstack/react-query": 4.43.0 - "@tanstack/react-query-devtools": 4.43.0 + "@tanstack/eslint-plugin-query": 5.91.4 + "@tanstack/react-query": 5.90.21 + "@tanstack/react-query-devtools": 5.91.3 "@testing-library/cypress": 10.1.0 "@testing-library/dom": 10.4.1 "@testing-library/jest-dom": 6.9.1 @@ -3709,8 +3704,9 @@ __metadata: "@emotion/styled": 11.14.1 "@mui/icons-material": 5.18.0 "@mui/material": 5.18.0 - "@tanstack/react-query": 4.43.0 - "@tanstack/react-query-devtools": 4.43.0 + "@tanstack/eslint-plugin-query": 5.91.4 + "@tanstack/react-query": 5.90.21 + "@tanstack/react-query-devtools": 5.91.3 "@testing-library/cypress": 10.1.0 "@testing-library/dom": 10.4.1 "@testing-library/jest-dom": 6.9.1 @@ -3782,8 +3778,9 @@ __metadata: "@mui/icons-material": 5.18.0 "@mui/material": 5.18.0 "@mui/x-date-pickers": 6.20.2 - "@tanstack/react-query": 4.43.0 - "@tanstack/react-query-devtools": 4.43.0 + "@tanstack/eslint-plugin-query": 5.91.4 + "@tanstack/react-query": 5.90.21 + "@tanstack/react-query-devtools": 5.91.3 "@testing-library/cypress": 10.1.0 "@testing-library/dom": 10.4.1 "@testing-library/jest-dom": 6.9.1 @@ -6098,13 +6095,6 @@ __metadata: languageName: node linkType: hard -"is-what@npm:^4.1.8": - version: 4.1.16 - resolution: "is-what@npm:4.1.16" - checksum: baf99e4b9f06003ceb3b2eea4a1e17179524ee3a6310dc44903eb675cfe3c0a17819ab057bb1ae6ba7ca4939ae4bdfcc6a0c4210a8457aff1756abd3607b713c - languageName: node - linkType: hard - "isarray@npm:0.0.1": version: 0.0.1 resolution: "isarray@npm:0.0.1" @@ -7912,13 +7902,6 @@ __metadata: languageName: node linkType: hard -"remove-accents@npm:0.5.0": - version: 0.5.0 - resolution: "remove-accents@npm:0.5.0" - checksum: 7045b37015acb03df406d21f9cbe93c3fcf2034189f5d2e33b1dace9c7d6bdcd839929905ced21a5d76c58553557e1a42651930728702312a5774179d5b9147b - languageName: node - linkType: hard - "request-progress@npm:^3.0.0": version: 3.0.0 resolution: "request-progress@npm:3.0.0" @@ -8808,15 +8791,6 @@ __metadata: languageName: node linkType: hard -"superjson@npm:^1.10.0": - version: 1.13.3 - resolution: "superjson@npm:1.13.3" - dependencies: - copy-anything: ^3.0.2 - checksum: f5aeb010f24163cb871a4bc402d9164112201c059afc247a75b03131c274aea6eec9cf08be9e4a9465fe4961689009a011584528531d52f7cc91c077e07e5c75 - languageName: node - linkType: hard - "supports-color@npm:^7.1.0": version: 7.2.0 resolution: "supports-color@npm:7.2.0" @@ -9271,7 +9245,7 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.0.0, use-sync-external-store@npm:^1.2.0, use-sync-external-store@npm:^1.6.0": +"use-sync-external-store@npm:^1.0.0": version: 1.6.0 resolution: "use-sync-external-store@npm:1.6.0" peerDependencies: