-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
UISACQCOMP-223: ECS - Add reusable custom hooks to fix invalid reference issues related to holding names and locations #824
Changes from 2 commits
29f1b36
aa686c7
fe3f9c4
fc8fe8b
e8abe63
cae8bfc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export { useHoldingsAndLocations } from './useHoldingsAndLocations'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
import { useQuery } from 'react-query'; | ||
|
||
import { | ||
useNamespace, | ||
useOkapiKy, | ||
} from '@folio/stripes/core'; | ||
|
||
import { LIMIT_MAX } from '../../constants'; | ||
import { | ||
getHoldingLocations, | ||
getHoldingLocationsByTenants, | ||
} from '../../utils'; | ||
|
||
const DEFAULT_DATA = []; | ||
|
||
export const useHoldingsAndLocations = ({ | ||
instanceId, | ||
options = {}, | ||
tenantId, | ||
/* | ||
`receivingTenantIds` is a unique list of tenantIds from the pieces list. | ||
The purpose is that we need to be able to fetch locations from other | ||
tenants so that we can display all the locations on the full-screen page | ||
*/ | ||
receivingTenantIds = DEFAULT_DATA, | ||
/* | ||
ECS mode: | ||
`additionalTenantLocationIdsMap` is a map of tenantId to locationIds for ECS mode. | ||
The format can be: { tenantId: [locationId1, locationId2] } | ||
The purpose is that we need to fetch newly added locations when we select a location | ||
from "Create new holdings for location" modal so that the value is displayed in the selection | ||
*/ | ||
additionalTenantLocationIdsMap = {}, | ||
/* | ||
Non-ECS mode: | ||
`additionalLocationIds` is a list of locationIds for the case when we need to fetch additional | ||
locations for the selected location in the form so that the value is displayed in the selection. | ||
*/ | ||
additionalLocationIds = [], | ||
}) => { | ||
const { enabled = true, ...queryOptions } = options; | ||
|
||
const ky = useOkapiKy({ tenant: tenantId }); | ||
const [namespace] = useNamespace({ key: 'holdings-and-location' }); | ||
const queryKey = [ | ||
namespace, | ||
tenantId, | ||
instanceId, | ||
...receivingTenantIds, | ||
...additionalLocationIds, | ||
...Object.values(additionalTenantLocationIdsMap), | ||
]; | ||
const searchParams = { | ||
query: `instanceId==${instanceId}`, | ||
limit: LIMIT_MAX, | ||
}; | ||
|
||
const { | ||
data, | ||
isFetching, | ||
isLoading, | ||
} = useQuery({ | ||
queryKey, | ||
queryFn: ({ signal }) => { | ||
return receivingTenantIds.length | ||
? getHoldingLocationsByTenants({ ky, instanceId, receivingTenantIds, additionalTenantLocationIdsMap }) | ||
: getHoldingLocations({ ky, searchParams, signal, additionalLocationIds }); | ||
}, | ||
enabled: enabled && Boolean(instanceId), | ||
...queryOptions, | ||
}); | ||
|
||
return ({ | ||
holdings: data?.holdings || DEFAULT_DATA, | ||
locations: data?.locations || DEFAULT_DATA, | ||
locationIds: data?.locationIds || DEFAULT_DATA, | ||
isFetching, | ||
isLoading, | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { | ||
QueryClient, | ||
QueryClientProvider, | ||
} from 'react-query'; | ||
|
||
import { | ||
renderHook, | ||
waitFor, | ||
} from '@testing-library/react-hooks'; | ||
import { useOkapiKy } from '@folio/stripes/core'; | ||
|
||
import { HOLDINGS_API } from '../../constants'; | ||
import { extendKyWithTenant } from '../../utils'; | ||
import { useHoldingsAndLocations } from './useHoldingsAndLocations'; | ||
|
||
jest.mock('../../utils', () => ({ | ||
...jest.requireActual('../../utils'), | ||
extendKyWithTenant: jest.fn().mockReturnValue({ extend: jest.fn() }), | ||
})); | ||
|
||
const queryClient = new QueryClient(); | ||
const wrapper = ({ children }) => ( | ||
<QueryClientProvider client={queryClient}> | ||
{children} | ||
</QueryClientProvider> | ||
); | ||
|
||
const holdingsRecords = [ | ||
{ | ||
'id': 'd7e99892-6d7d-46eb-8a4c-3aa471785819', | ||
'instanceId': '8804aeeb-db18-4c42-b0e9-28d63c7855e6', | ||
'permanentLocationId': '9d1b77e8-f02e-4b7f-b296-3f2042ddac54', | ||
}, | ||
{ | ||
'id': '5636949f-8f97-4b25-a0fc-90fdb050ffb0', | ||
'instanceId': '8804aeeb-db18-4c42-b0e9-28d63c7855e6', | ||
'permanentLocationId': '9d1b77e8-f02e-4b7f-b296-3f2042ddac54', | ||
}, | ||
]; | ||
|
||
const locations = [ | ||
{ | ||
'id': '9d1b77e8-f02e-4b7f-b296-3f2042ddac54', | ||
'name': 'DCB', | ||
'code': '000', | ||
}, | ||
]; | ||
|
||
const getMock = jest.fn() | ||
.mockReturnValueOnce({ json: () => Promise.resolve({ holdingsRecords }) }) | ||
.mockReturnValue({ json: () => Promise.resolve({ locations }) }); | ||
|
||
describe('useHoldingsAndLocations', () => { | ||
beforeEach(() => { | ||
useOkapiKy | ||
.mockClear() | ||
.mockReturnValue({ | ||
get: getMock, | ||
extend: jest.fn(() => ({ | ||
get: jest.fn((path) => { | ||
if (path === HOLDINGS_API) { | ||
return { | ||
json: jest.fn().mockResolvedValue({ holdingsRecords }), | ||
}; | ||
} | ||
|
||
return ({ | ||
json: jest.fn().mockResolvedValue({ locations }), | ||
}); | ||
}), | ||
})), | ||
}); | ||
extendKyWithTenant.mockClear().mockReturnValue({ extend: jest.fn() }); | ||
}); | ||
|
||
it('should fetch holding locations', async () => { | ||
const { result } = renderHook(() => useHoldingsAndLocations({ instanceId: '1', tenantId: '2' }), { wrapper }); | ||
|
||
await waitFor(() => expect(result.current.isLoading).toBeFalsy()); | ||
|
||
expect(result.current.locations).toEqual(locations); | ||
}); | ||
|
||
it('should fetch holding locations with different tenants', async () => { | ||
const receivingTenantIds = ['1', '2']; | ||
const { result } = renderHook(() => useHoldingsAndLocations({ instanceId: '1', receivingTenantIds, tenantId: '2' }), { wrapper }); | ||
|
||
await waitFor(() => expect(result.current.isLoading).toBeFalsy()); | ||
|
||
expect(result.current.locations).toHaveLength(2); | ||
}); | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './useReceivingTenantIdsAndLocations'; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
import uniq from 'lodash/uniq'; | ||
import { useMemo } from 'react'; | ||
|
||
import { useCurrentUserTenants } from '../consortia'; | ||
|
||
export const useReceivingTenantIdsAndLocations = ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could you please leave the purpose of this hook in the comments? |
||
currentReceivingTenantId, | ||
currentLocationId: locationId, | ||
receivingTenantIds = [], | ||
}) => { | ||
const currentUserTenants = useCurrentUserTenants(); | ||
|
||
const receivingTenants = useMemo(() => { | ||
if (receivingTenantIds.length) { | ||
const currentUserTenantIds = currentUserTenants?.map(({ id: tenantId }) => tenantId); | ||
|
||
// should get unique tenantIds from poLine locations and filter out tenantIds where the current user has assigned | ||
return uniq([ | ||
...receivingTenantIds, | ||
currentReceivingTenantId, | ||
].filter((tenantId) => currentUserTenantIds?.includes(tenantId)) | ||
.filter(Boolean)); | ||
} | ||
|
||
return []; | ||
}, [receivingTenantIds, currentUserTenants, currentReceivingTenantId]); | ||
|
||
const additionalLocations = useMemo(() => { | ||
const locationIds = locationId ? [locationId] : []; | ||
const tenantLocationIdsMap = currentReceivingTenantId ? { [currentReceivingTenantId]: locationIds } : {}; | ||
|
||
return { | ||
additionalLocationIds: locationIds, | ||
additionalTenantLocationIdsMap: tenantLocationIdsMap, | ||
}; | ||
}, [locationId, currentReceivingTenantId]); | ||
|
||
return { | ||
receivingTenantIds: receivingTenants, | ||
tenantId: currentReceivingTenantId, | ||
...additionalLocations, | ||
}; | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
import { OKAPI_TENANT_HEADER } from '../constants'; | ||
|
||
export const extendKyWithTenant = (ky, tenantId) => { | ||
return ky.extend({ | ||
hooks: { | ||
beforeRequest: [ | ||
request => { | ||
request.headers.set(OKAPI_TENANT_HEADER, tenantId); | ||
}, | ||
], | ||
}, | ||
}); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import { | ||
HOLDINGS_API, | ||
LIMIT_MAX, | ||
LOCATIONS_API, | ||
} from '../constants'; | ||
import { batchRequest } from './batchFetch'; | ||
import { extendKyWithTenant } from './extendKyWithTenant'; | ||
|
||
const DEFAULT_DATA = []; | ||
|
||
export const getHoldingLocations = async ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on the name of this function, I expect to get a list of locations for a specific holding according to certain parameters, not for a list of holdings. |
||
ky, | ||
searchParams, | ||
signal, | ||
/* | ||
Non-ECS mode: | ||
`additionalLocationIds` is a list of locationIds for the case when we need to fetch additional | ||
locations for the selected location in the form so that the value is displayed in the selection. | ||
*/ | ||
additionalLocationIds = [], | ||
tenantId, | ||
}) => { | ||
const holdings = await ky | ||
.get(HOLDINGS_API, { searchParams, signal }) | ||
.json() | ||
.then(response => { | ||
return tenantId ? | ||
response.holdingsRecords.map(holding => ({ ...holding, tenantId })) | ||
: response.holdingsRecords; | ||
}); | ||
|
||
const locationIds = holdings?.map(({ permanentLocationId }) => permanentLocationId) || DEFAULT_DATA; | ||
const uniqueLocationIds = [...new Set([...locationIds, ...additionalLocationIds])]; | ||
|
||
const locations = await batchRequest( | ||
({ params }) => ky.get(LOCATIONS_API, { searchParams: params, signal }).json(), | ||
uniqueLocationIds, | ||
).then(responses => responses.flatMap(response => { | ||
return tenantId | ||
? response.locations.map(location => ({ ...location, tenantId })) | ||
: response.locations; | ||
})); | ||
|
||
return { | ||
holdings, | ||
locations, | ||
locationIds, | ||
}; | ||
}; | ||
|
||
export const getHoldingLocationsByTenants = async ({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. similar here |
||
ky, | ||
instanceId, | ||
/* | ||
ECS mode: | ||
`additionalTenantLocationIdsMap` is a map of tenantId to locationIds for ECS mode. | ||
The format can be: { tenantId: [locationId1, locationId2] } | ||
The purpose is that we need to fetch newly added locations when we select a location | ||
from "Create new holdings for location" modal so that the value is displayed in the selection | ||
*/ | ||
additionalTenantLocationIdsMap = {}, | ||
receivingTenantIds = DEFAULT_DATA, | ||
}) => { | ||
const searchParams = { | ||
query: `instanceId==${instanceId}`, | ||
limit: LIMIT_MAX, | ||
}; | ||
|
||
if (!receivingTenantIds.length) { | ||
return { | ||
locations: DEFAULT_DATA, | ||
locationIds: DEFAULT_DATA, | ||
holdings: DEFAULT_DATA, | ||
}; | ||
} | ||
|
||
const locationsRequest = receivingTenantIds.map(async (tenantId) => { | ||
const tenantKy = extendKyWithTenant(ky, tenantId); | ||
|
||
return getHoldingLocations({ | ||
ky: tenantKy, | ||
searchParams, | ||
tenantId, | ||
additionalLocationIds: additionalTenantLocationIdsMap[tenantId], | ||
}); | ||
}); | ||
|
||
const locationsResponse = await Promise.all(locationsRequest).then(responses => { | ||
return responses.reduce((acc, item) => { | ||
return { | ||
locations: acc.locations.concat(item.locations), | ||
locationIds: acc.locationIds.concat(item.locationIds), | ||
holdings: acc.holdings.concat(item.holdings), | ||
}; | ||
}, { locations: DEFAULT_DATA, locationIds: DEFAULT_DATA, holdings: DEFAULT_DATA }); | ||
}); | ||
|
||
return locationsResponse; | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The hook's name is too general given that it requires many specific arguments.