diff --git a/CHANGELOG.md b/CHANGELOG.md index 9e1ca147..bf5c453c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## (6.1.0 IN PROGRESS) +* Add more reusable hooks and utilities. Refs UISACQCOMP-228. + ## (6.0.1 IN PROGRESS) * Added new `subscribeOnReset` prop to ``. Refs UISACQCOMP-227. diff --git a/lib/AcqList/utils/queryUtils.js b/lib/AcqList/utils/queryUtils.js index 9118c14f..86e58046 100644 --- a/lib/AcqList/utils/queryUtils.js +++ b/lib/AcqList/utils/queryUtils.js @@ -31,7 +31,7 @@ export const buildArrayFieldQuery = (filterKey, filterValue) => { }; export const buildDateRangeQuery = (filterKey, filterValue) => { - const [from, to] = filterValue.split(':'); + const [from, to] = (Array.isArray(filterValue) ? filterValue.toString() : filterValue).split(':'); const start = moment(from).startOf('day').format(DATE_RANGE_FILTER_FORMAT); const end = moment(to).endOf('day').format(DATE_RANGE_FILTER_FORMAT); @@ -47,7 +47,7 @@ export const buildNumberRangeQuery = (filterKey, filterValue) => { }; export const buildDateTimeRangeQuery = (filterKey, filterValue) => { - const [from, to] = filterValue.split(':'); + const [from, to] = (Array.isArray(filterValue) ? filterValue.toString() : filterValue).split(':'); const start = moment(from).startOf('day').utc().format(DATE_RANGE_FILTER_FORMAT); const end = moment(to).endOf('day').utc().format(DATE_RANGE_FILTER_FORMAT); @@ -60,7 +60,11 @@ export const getFilterParams = (queryParams) => omit( ); export const getFiltersCount = (filters) => { - return filters && Object.keys(getFilterParams(filters)).filter(k => filters[k] !== undefined).length; + return filters && Object.keys(getFilterParams(filters)).filter((k) => { + return k === SEARCH_PARAMETER + ? Boolean(filters[k]) + : filters[k] !== undefined; + }).length; }; export const buildFilterQuery = ( diff --git a/lib/CustomFieldsFilters/CustomFieldsFilter.js b/lib/CustomFieldsFilters/CustomFieldsFilter.js index 21634dc4..84658689 100644 --- a/lib/CustomFieldsFilters/CustomFieldsFilter.js +++ b/lib/CustomFieldsFilters/CustomFieldsFilter.js @@ -26,11 +26,12 @@ const CustomFieldsFilter = ({ customField, disabled, onChange, + name: filterNameProp, }) => { const FilterComponent = customFieldTypeToFilterMap[customField.type]; const { refId, name, selectField } = customField; const values = selectField?.options?.values ?? [{ id: 'true', value: name }]; - const filterName = `${CUSTOM_FIELDS_FILTER}.${refId}`; + const filterName = `${filterNameProp || CUSTOM_FIELDS_FILTER}.${refId}`; const dataOptions = values.map(({ id: value, value: label }) => ({ label, value, @@ -85,6 +86,7 @@ CustomFieldsFilter.propTypes = { customField: PropTypes.object, disabled: PropTypes.bool, onChange: PropTypes.func.isRequired, + name: PropTypes.string, }; CustomFieldsFilter.defaultProps = { diff --git a/lib/CustomFieldsFilters/CustomFieldsFilters.js b/lib/CustomFieldsFilters/CustomFieldsFilters.js index 72ddb8bb..c6779613 100644 --- a/lib/CustomFieldsFilters/CustomFieldsFilters.js +++ b/lib/CustomFieldsFilters/CustomFieldsFilters.js @@ -7,13 +7,15 @@ export const CustomFieldsFilters = ({ activeFilters, customFields, onChange, + name, ...props }) => { if (!customFields) return null; return customFields.map((customField) => ( { + const { + enabled = true, + tenantId, + ...queryOptions + } = options; + + const [namespace] = useNamespace({ key: 'acquisitions-units' }); + const ky = useOkapiKy({ tenant: tenantId }); + + const searchParams = { + query: `(module=${MODULE_TENANT} and configName=${CONFIG_ADDRESSES})`, + limit: LIMIT_MAX, + }; + + const { + data, + isFetching, + isLoading, + } = useQuery({ + queryKey: [namespace, tenantId], + queryFn: ({ signal }) => { + return ky.get(CONFIG_API, { searchParams, signal }) + .json() + .then(({ configs, totalRecords }) => ({ + addresses: getAddresses(configs), + totalRecords, + })); + }, + enabled, + ...queryOptions, + }); + + return ({ + addresses: data?.addresses || DEFAULT_DATA, + totalRecords: data?.totalRecords || 0, + isFetching, + isLoading, + }); +}; diff --git a/lib/hooks/useAddresses/useAddresses.test.js b/lib/hooks/useAddresses/useAddresses.test.js new file mode 100644 index 00000000..fb3f673e --- /dev/null +++ b/lib/hooks/useAddresses/useAddresses.test.js @@ -0,0 +1,51 @@ +import { renderHook } from '@testing-library/react-hooks'; +import { + QueryClient, + QueryClientProvider, +} from 'react-query'; + +import { useOkapiKy } from '@folio/stripes/core'; + +import { getAddresses } from '../../utils'; +import { useAddresses } from './useAddresses'; + +jest.mock('../../utils', () => ({ + getAddresses: jest.fn(), +})); + +const queryClient = new QueryClient(); +const wrapper = ({ children }) => ( + + {children} + +); + +describe('useAddresses', () => { + const mockGet = jest.fn(); + + beforeEach(() => { + jest.clearAllMocks(); + + useOkapiKy.mockReturnValue({ + get: mockGet, + }); + }); + + it('should return addresses and totalRecords', async () => { + const configs = [{ value: 'address1' }, { value: 'address2' }]; + const totalRecords = 2; + + mockGet.mockReturnValue({ + json: jest.fn().mockResolvedValue({ configs, totalRecords }), + }); + + getAddresses.mockReturnValue(['address1', 'address2']); + + const { result, waitFor } = renderHook(() => useAddresses(), { wrapper }); + + await waitFor(() => !result.current.isLoading); + + expect(result.current.addresses).toEqual(['address1', 'address2']); + expect(result.current.totalRecords).toBe(2); + }); +}); diff --git a/lib/utils/api/api.test.js b/lib/utils/api/api.test.js new file mode 100644 index 00000000..4dc5baad --- /dev/null +++ b/lib/utils/api/api.test.js @@ -0,0 +1,113 @@ +import { + LIMIT_PARAMETER, + SEARCH_PARAMETER, +} from '../../AcqList/constants'; +import { + LIMIT_MAX, + LINES_API, + ORDER_PIECES_API, + ORDERS_API, + RECEIVING_TITLES_API, + VENDORS_API, +} from '../../constants'; + +import { fetchOrderLines } from './fetchOrderLines'; +import { fetchOrderLinesByIds } from './fetchOrderLinesByIds'; +import { fetchOrders } from './fetchOrders'; +import { fetchOrdersByIds } from './fetchOrdersByIds'; +import { fetchOrganizations } from './fetchOrganizations'; +import { fetchOrganizationsByIds } from './fetchOrganizationsByIds'; +import { fetchPieces } from './fetchPieces'; +import { fetchReceivingTitles } from './fetchReceivingTitles'; +import { fetchReceivingTitlesByIds } from './fetchReceivingTitlesByIds'; + +const httpClient = { + get: jest.fn(() => ({ + json: jest.fn(() => Promise.resolve({})), + })), +}; + +const ids = ['1', '2']; +const options = {}; +const searchParams = { + [LIMIT_PARAMETER]: LIMIT_MAX, + [SEARCH_PARAMETER]: 'id==1 or id==2', +}; + +describe('API utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchOrderLines', () => { + it('should fetch order lines', async () => { + await fetchOrderLines(httpClient)(options); + + expect(httpClient.get).toHaveBeenCalledWith(LINES_API, options); + }); + }); + + describe('fetchOrderLinesByIds', () => { + it('should fetch order lines by ids', async () => { + await fetchOrderLinesByIds(httpClient)(ids, options); + + expect(httpClient.get).toHaveBeenCalledWith(LINES_API, { searchParams }); + }); + }); + + describe('fetchOrders', () => { + it('should fetch orders', async () => { + await fetchOrders(httpClient)(options); + + expect(httpClient.get).toHaveBeenCalledWith(ORDERS_API, options); + }); + }); + + describe('fetchOrdersByIds', () => { + it('should fetch orders by ids', async () => { + await fetchOrdersByIds(httpClient)(ids, options); + + expect(httpClient.get).toHaveBeenCalledWith(ORDERS_API, { searchParams }); + }); + }); + + describe('fetchOrganizations', () => { + it('should fetch organizations', async () => { + await fetchOrganizations(httpClient)(options); + + expect(httpClient.get).toHaveBeenCalledWith(VENDORS_API, options); + }); + }); + + describe('fetchOrganizationsByIds', () => { + it('should fetch organizations by ids', async () => { + await fetchOrganizationsByIds(httpClient)(ids, options); + + expect(httpClient.get).toHaveBeenCalledWith(VENDORS_API, { searchParams }); + }); + }); + + describe('fetchPieces', () => { + it('should fetch pieces', async () => { + await fetchPieces(httpClient)(options); + + expect(httpClient.get).toHaveBeenCalledWith(ORDER_PIECES_API, options); + }); + }); + + describe('fetchReceivingTitles', () => { + it('should fetch receiving titles', async () => { + await fetchReceivingTitles(httpClient)(options); + + expect(httpClient.get).toHaveBeenCalledWith(RECEIVING_TITLES_API, options); + }); + }); + + describe('fetchReceivingTitlesByIds', () => { + it('should fetch receiving titles by ids', async () => { + await fetchReceivingTitlesByIds(httpClient)(ids, options); + + expect(httpClient.get).toHaveBeenCalledWith(RECEIVING_TITLES_API, { searchParams }); + }); + }); +}); diff --git a/lib/utils/api/fetchOrderLines.js b/lib/utils/api/fetchOrderLines.js new file mode 100644 index 00000000..03314a1e --- /dev/null +++ b/lib/utils/api/fetchOrderLines.js @@ -0,0 +1,5 @@ +import { LINES_API } from '../../constants'; + +export const fetchOrderLines = (httpClient) => async (options) => { + return httpClient.get(LINES_API, options).json(); +}; diff --git a/lib/utils/api/fetchOrderLinesByIds.js b/lib/utils/api/fetchOrderLinesByIds.js new file mode 100644 index 00000000..ba3eda57 --- /dev/null +++ b/lib/utils/api/fetchOrderLinesByIds.js @@ -0,0 +1,19 @@ +import { batchRequest } from '../batchFetch'; +import { fetchOrderLines } from './fetchOrderLines'; + +export const fetchOrderLinesByIds = (httpClient) => { + return (ids, options) => { + const requestFn = ({ params }) => { + return fetchOrderLines(httpClient)({ ...options, searchParams: params }); + }; + + return batchRequest(requestFn, ids).then((responses) => { + return responses.reduce((acc, response) => { + return { + poLines: acc.poLines.concat(response.poLines), + totalRecords: acc.totalRecords + response.totalRecords, + }; + }, { poLines: [], totalRecords: 0 }); + }); + }; +}; diff --git a/lib/utils/api/fetchOrders.js b/lib/utils/api/fetchOrders.js new file mode 100644 index 00000000..b65f7c33 --- /dev/null +++ b/lib/utils/api/fetchOrders.js @@ -0,0 +1,5 @@ +import { ORDERS_API } from '../../constants'; + +export const fetchOrders = (httpClient) => async (options) => { + return httpClient.get(ORDERS_API, options).json(); +}; diff --git a/lib/utils/api/fetchOrdersByIds.js b/lib/utils/api/fetchOrdersByIds.js new file mode 100644 index 00000000..25c10965 --- /dev/null +++ b/lib/utils/api/fetchOrdersByIds.js @@ -0,0 +1,19 @@ +import { batchRequest } from '../batchFetch'; +import { fetchOrders } from './fetchOrders'; + +export const fetchOrdersByIds = (httpClient) => { + return (ids, options) => { + const requestFn = ({ params }) => { + return fetchOrders(httpClient)({ ...options, searchParams: params }); + }; + + return batchRequest(requestFn, ids).then((responses) => { + return responses.reduce((acc, response) => { + return { + purchaseOrders: acc.purchaseOrders.concat(response.purchaseOrders), + totalRecords: acc.totalRecords + response.totalRecords, + }; + }, { purchaseOrders: [], totalRecords: 0 }); + }); + }; +}; diff --git a/lib/utils/api/fetchOrganizations.js b/lib/utils/api/fetchOrganizations.js new file mode 100644 index 00000000..000dbdf6 --- /dev/null +++ b/lib/utils/api/fetchOrganizations.js @@ -0,0 +1,7 @@ +import { VENDORS_API } from '../../constants'; + +export const fetchOrganizations = (httpClient) => { + return async (options) => { + return httpClient.get(VENDORS_API, options).json(); + }; +}; diff --git a/lib/utils/api/fetchOrganizationsByIds.js b/lib/utils/api/fetchOrganizationsByIds.js new file mode 100644 index 00000000..56eefb62 --- /dev/null +++ b/lib/utils/api/fetchOrganizationsByIds.js @@ -0,0 +1,19 @@ +import { batchRequest } from '../batchFetch'; +import { fetchOrganizations } from './fetchOrganizations'; + +export const fetchOrganizationsByIds = (httpClient) => { + return (ids, options) => { + const requestFn = ({ params }) => { + return fetchOrganizations(httpClient)({ ...options, searchParams: params }); + }; + + return batchRequest(requestFn, ids).then((responses) => { + return responses.reduce((acc, response) => { + return { + organizations: acc.organizations.concat(response.organizations), + totalRecords: acc.totalRecords + response.totalRecords, + }; + }, { organizations: [], totalRecords: 0 }); + }); + }; +}; diff --git a/lib/utils/api/fetchPieces.js b/lib/utils/api/fetchPieces.js new file mode 100644 index 00000000..7de8e1c0 --- /dev/null +++ b/lib/utils/api/fetchPieces.js @@ -0,0 +1,5 @@ +import { ORDER_PIECES_API } from '../../constants'; + +export const fetchPieces = (httpClient) => async (options) => { + return httpClient.get(ORDER_PIECES_API, options).json(); +}; diff --git a/lib/utils/api/fetchReceivingTitles.js b/lib/utils/api/fetchReceivingTitles.js new file mode 100644 index 00000000..58563e70 --- /dev/null +++ b/lib/utils/api/fetchReceivingTitles.js @@ -0,0 +1,7 @@ +import { RECEIVING_TITLES_API } from '../../constants'; + +export const fetchReceivingTitles = (httpClient) => { + return async (options) => { + return httpClient.get(RECEIVING_TITLES_API, options).json(); + }; +}; diff --git a/lib/utils/api/fetchReceivingTitlesByIds.js b/lib/utils/api/fetchReceivingTitlesByIds.js new file mode 100644 index 00000000..4ad939a2 --- /dev/null +++ b/lib/utils/api/fetchReceivingTitlesByIds.js @@ -0,0 +1,19 @@ +import { batchRequest } from '../batchFetch'; +import { fetchReceivingTitles } from './fetchReceivingTitles'; + +export const fetchReceivingTitlesByIds = (httpClient) => { + return (ids, options) => { + const requestFn = ({ params }) => { + return fetchReceivingTitles(httpClient)({ ...options, searchParams: params }); + }; + + return batchRequest(requestFn, ids).then((responses) => { + return responses.reduce((acc, response) => { + return { + titles: acc.titles.concat(response.titles), + totalRecords: acc.totalRecords + response.totalRecords, + }; + }, { titles: [], totalRecords: 0 }); + }); + }; +}; diff --git a/lib/utils/api/index.js b/lib/utils/api/index.js new file mode 100644 index 00000000..959d968f --- /dev/null +++ b/lib/utils/api/index.js @@ -0,0 +1,9 @@ +export { fetchOrderLines } from './fetchOrderLines'; +export { fetchOrderLinesByIds } from './fetchOrderLinesByIds'; +export { fetchOrders } from './fetchOrders'; +export { fetchOrdersByIds } from './fetchOrdersByIds'; +export { fetchOrganizations } from './fetchOrganizations'; +export { fetchOrganizationsByIds } from './fetchOrganizationsByIds'; +export { fetchPieces } from './fetchPieces'; +export { fetchReceivingTitles } from './fetchReceivingTitles'; +export { fetchReceivingTitlesByIds } from './fetchReceivingTitlesByIds'; diff --git a/lib/utils/getAddressOptions.js b/lib/utils/getAddressOptions.js new file mode 100644 index 00000000..6abba119 --- /dev/null +++ b/lib/utils/getAddressOptions.js @@ -0,0 +1,8 @@ +import sortBy from 'lodash/sortBy'; + +export const getAddressOptions = (addresses = []) => sortBy( + addresses.map(address => ({ + value: address.id, + label: address.name, + })), 'label', +); diff --git a/lib/utils/index.js b/lib/utils/index.js index efd2c9a3..b29a795f 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -1,4 +1,5 @@ export * from './acqRowFormatter'; +export * from './api'; export * from './batchFetch'; export * from './calculateFundAmount'; export * from './consortia'; @@ -16,6 +17,7 @@ export * from './formatDateTime'; export * from './generateQueryTemplate'; export * from './getAcqUnitsOptions'; export * from './getAddresses'; +export * from './getAddressOptions'; export * from './getAmountWithCurrency'; export * from './getConfigSetting'; export * from './getControlledVocabTranslations';