diff --git a/CHANGELOG.md b/CHANGELOG.md
index 842eae30..3f9affa2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,6 +38,7 @@
* [UIBULKED-400](https://issues.folio.org/browse/UIBULKED-400) Enabling Build query button on Query tab.
* [UIBULKED-399](https://issues.folio.org/browse/UIBULKED-399) Update label verbiage on the Identifier tab.
* [UIBULKED-407](https://issues.folio.org/browse/UIBULKED-407) When selecting action "Find" in Bulk Edit, the last dropdown on the row moves to the right.
+* [UIBULKED-403](https://issues.folio.org/browse/UIBULKED-403) Integrate query-plugin with bulk-edit API.
## [4.0.0](https://github.com/folio-org/ui-bulk-edit/tree/v4.0.0) (2023-10-12)
diff --git a/src/components/BulkEditList/BulkEditList.test.js b/src/components/BulkEditList/BulkEditList.test.js
index 1eb3908e..708f0622 100644
--- a/src/components/BulkEditList/BulkEditList.test.js
+++ b/src/components/BulkEditList/BulkEditList.test.js
@@ -5,17 +5,14 @@ import { QueryClientProvider } from 'react-query';
import '../../../test/jest/__mock__';
+import userEvent from '@testing-library/user-event';
+import { useOkapiKy } from '@folio/stripes/core';
import { queryClient } from '../../../test/jest/utils/queryClient';
import { CAPABILITIES, IDENTIFIERS, CRITERIA } from '../../constants';
import { BulkEditList } from './BulkEditList';
-jest.mock('./BulkEditListFilters/BulkEditListFilters', () => {
- return {
- BulkEditListFilters: jest.fn().mockReturnValue('BulkEditListFilters'),
- };
-});
jest.mock('../BulkEditLogs/BulkEditLogs', () => {
return jest.fn().mockReturnValue('BulkEditLogs');
});
@@ -24,6 +21,10 @@ jest.mock('./BulkEditListResult', () => {
BulkEditListResult: jest.fn().mockReturnValue('BulkEditListResult'),
};
});
+
+jest.mock('@folio/stripes/core', () => ({
+ useOkapiKy: jest.fn(),
+}));
jest.mock('./BulkEditListResult/BulkEditManualUploadModal', () => {
return {
BulkEditManualUploadModal: jest.fn().mockReturnValue('BulkEditManualUploadModal'),
@@ -53,7 +54,7 @@ describe('BulkEditList', () => {
it('should display Filters pane', async () => {
renderBulkEditList({ criteria: CRITERIA.LOGS });
- expect(screen.getByText('BulkEditListFilters')).toBeVisible();
+ expect(screen.getByText(/holdings/i)).toBeVisible();
});
it('should display Logs pane when criteria is logs', async () => {
@@ -67,4 +68,18 @@ describe('BulkEditList', () => {
expect(screen.getByText(/BulkEditListResult/)).toBeVisible();
});
+
+ it('should display Bulk edit query', async () => {
+ useOkapiKy.mockReturnValue({
+ get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({}) }),
+ post: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({}) }),
+ delete: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({}) }),
+ });
+ renderBulkEditList({ criteria: CRITERIA.QUERY });
+
+ userEvent.click(screen.getByText(/Get query/));
+ userEvent.click(screen.getByText(/Cancel query/));
+
+ expect(screen.getByText(/holdings/i)).toBeVisible();
+ });
});
diff --git a/src/components/BulkEditList/BulkEditListFilters/BulkEditListFilters.js b/src/components/BulkEditList/BulkEditListFilters/BulkEditListFilters.js
index 499fd9cb..af6821e5 100644
--- a/src/components/BulkEditList/BulkEditListFilters/BulkEditListFilters.js
+++ b/src/components/BulkEditList/BulkEditListFilters/BulkEditListFilters.js
@@ -26,7 +26,7 @@ import {
import { RootContext } from '../../../context/RootContext';
import {
useUpload,
- useBulkOperationStart,
+ useBulkOperationStart, useQueryPlugin,
} from '../../../hooks/api';
import { useBulkPermissions, useLocationFilters } from '../../../hooks';
import { LogsFilters } from './LogsFilters/LogsFilters';
@@ -34,6 +34,8 @@ import { getCapabilityOptions, isCapabilityDisabled } from '../../../utils/helpe
import FilterTabs from './FilterTabs/FilterTabs';
import Capabilities from './Capabilities/Capabilities';
import { getIsDisabledByPerm } from './utils/getIsDisabledByPerm';
+import { useRecordTypes } from '../../../hooks/api/useRecordTypes';
+import { getRecordType } from '../../../utils/getRecordType';
export const BulkEditListFilters = ({
filters,
@@ -65,6 +67,7 @@ export const BulkEditListFilters = ({
const initialCapabilities = search.get('capabilities');
const initialFileName = search.get('fileName');
const initialStep = search.get('step');
+ const initialRecordType = search.get('recordTypes');
const logFilters = Object.values(FILTERS).map((el) => search.getAll(el));
const isQuery = criteria === CRITERIA.QUERY;
@@ -83,6 +86,7 @@ export const BulkEditListFilters = ({
criteria: CRITERIA.LOGS,
fileName: initialFileName,
step: initialStep,
+ recordTypes: initialRecordType,
};
const [
@@ -105,6 +109,16 @@ export const BulkEditListFilters = ({
const { fileUpload, isLoading } = useUpload();
const { bulkOperationStart } = useBulkOperationStart();
+ const { recordTypes } = useRecordTypes();
+
+ const {
+ entityTypeDataSource,
+ queryDetailsDataSource,
+ testQueryDataSource,
+ getParamsSource,
+ cancelQueryDataSource,
+ } = useQueryPlugin(initialRecordType);
+
const handleChange = (value, field) => setFilters(prev => ({
...prev, [field]: value,
}));
@@ -118,6 +132,8 @@ export const BulkEditListFilters = ({
recordIdentifier: '',
}));
+ const selected = recordTypes?.find(type => type.label === getRecordType(value))?.id;
+
history.replace({
pathname: '/bulk-edit',
search: buildSearch({
@@ -125,6 +141,7 @@ export const BulkEditListFilters = ({
identifier: null,
step: null,
fileName: null,
+ recordTypes: selected
}, location.search),
});
@@ -312,6 +329,13 @@ export const BulkEditListFilters = ({
componentType="builder"
type="query-builder"
disabled={isQueryBuilderDisabled}
+ key={capabilities}
+ entityTypeDataSource={entityTypeDataSource}
+ testQueryDataSource={testQueryDataSource}
+ getParamsSource={getParamsSource}
+ queryDetailsDataSource={queryDetailsDataSource}
+ onQueryRunFail={() => {}}
+ cancelQueryDataSource={cancelQueryDataSource}
/>
>
)}
diff --git a/src/constants/core.js b/src/constants/core.js
index fd826bf5..03907aa0 100644
--- a/src/constants/core.js
+++ b/src/constants/core.js
@@ -20,6 +20,11 @@ export const CAPABILITIES = {
USER: 'USER',
};
+export const RECORD_TYPES = {
+ ITEM: 'Items',
+ USER: 'Users'
+};
+
export const IDENTIFIERS = {
ID: 'ID',
BARCODE: 'BARCODE',
diff --git a/src/hooks/api/index.js b/src/hooks/api/index.js
index 10f1a360..27ec39ca 100644
--- a/src/hooks/api/index.js
+++ b/src/hooks/api/index.js
@@ -9,3 +9,4 @@ export * from './useUserGroupsMap';
export * from './usePatronGroup';
export * from './useLoanTypes';
export * from './useBulkOperationDetails';
+export * from './useQueryPlugin';
diff --git a/src/hooks/api/useQueryPlugin.js b/src/hooks/api/useQueryPlugin.js
new file mode 100644
index 00000000..6e63911b
--- /dev/null
+++ b/src/hooks/api/useQueryPlugin.js
@@ -0,0 +1,50 @@
+import { useOkapiKy } from '@folio/stripes/core';
+
+export const useQueryPlugin = (recordType) => {
+ const ky = useOkapiKy();
+
+ const entityTypeDataSource = async () => {
+ if (recordType) {
+ const response = ky.get(`entity-types/${recordType}`);
+ return response.json();
+ }
+ return null;
+ };
+
+ const queryDetailsDataSource = async ({ queryId, includeContent, offset, limit }) => {
+ const searchParams = {
+ includeResults: includeContent,
+ offset,
+ limit
+ };
+
+ const response = ky.get(`query/${queryId}`, { searchParams });
+
+ return response.json();
+ };
+
+ const testQueryDataSource = async ({ fqlQuery }) => {
+ const response = ky.post('query', { json: {
+ entityTypeId: recordType,
+ fqlQuery: JSON.stringify(fqlQuery)
+ } });
+ return response.json();
+ };
+
+ const getParamsSource = async ({ entityTypeId, columnName, searchValue }) => {
+ const response = ky.get(`entity-types/${entityTypeId}/columns/${columnName}/values?search=${searchValue}`);
+ return response.json();
+ };
+
+ const cancelQueryDataSource = async ({ queryId }) => {
+ return ky.delete(`query/${queryId}`);
+ };
+
+ return {
+ entityTypeDataSource,
+ queryDetailsDataSource,
+ testQueryDataSource,
+ getParamsSource,
+ cancelQueryDataSource,
+ };
+};
diff --git a/src/hooks/api/useRecordTypes.js b/src/hooks/api/useRecordTypes.js
new file mode 100644
index 00000000..19a30cce
--- /dev/null
+++ b/src/hooks/api/useRecordTypes.js
@@ -0,0 +1,23 @@
+import { useOkapiKy } from '@folio/stripes/core';
+import { useQuery } from 'react-query';
+
+const ENTITY_TYPE_HASH = 'entityType';
+
+export const useRecordTypes = () => {
+ const ky = useOkapiKy();
+ const { data, isLoading, error } = useQuery({
+ queryKey: [ENTITY_TYPE_HASH],
+ queryFn: async () => {
+ const response = await ky.get('entity-types');
+
+ return response.json();
+ },
+ refetchOnWindowFocus: false,
+ });
+
+ return ({
+ recordTypes: data,
+ isLoading,
+ error
+ });
+};
diff --git a/src/hooks/api/useRecordTypes.test.js b/src/hooks/api/useRecordTypes.test.js
new file mode 100644
index 00000000..15cef392
--- /dev/null
+++ b/src/hooks/api/useRecordTypes.test.js
@@ -0,0 +1,63 @@
+import { renderHook } from '@testing-library/react-hooks';
+import { useOkapiKy } from '@folio/stripes/core';
+import { useQuery } from 'react-query';
+import { act } from '@testing-library/react';
+import { useRecordTypes } from './useRecordTypes';
+
+jest.mock('@folio/stripes/core', () => ({
+ useOkapiKy: jest.fn(),
+}));
+
+jest.mock('react-query', () => ({
+ ...jest.requireActual('react-query'),
+ useQuery: jest.fn(),
+}));
+
+describe('useRecordTypes', () => {
+ it('should return recordTypes, loading state, and error state', async () => {
+ useOkapiKy.mockReturnValue({
+ get: jest.fn().mockResolvedValue({ json: jest.fn().mockResolvedValue({}) }),
+ });
+
+ useQuery.mockReturnValue({
+ data: [
+ {
+ 'id': '1',
+ 'label': 'Loans'
+ },
+ {
+ 'id': '2',
+ 'label': 'Users'
+ },
+ {
+ 'id': '3',
+ 'label': 'Items'
+ }
+ ],
+ isLoading: false,
+ error: null,
+ });
+
+ let result;
+ await act(async () => {
+ result = renderHook(() => useRecordTypes()).result;
+ });
+
+ expect(result.current.recordTypes).toEqual([
+ {
+ 'id': '1',
+ 'label': 'Loans'
+ },
+ {
+ 'id': '2',
+ 'label': 'Users'
+ },
+ {
+ 'id': '3',
+ 'label': 'Items'
+ }
+ ]);
+ expect(result.current.isLoading).toBeFalsy();
+ expect(result.current.error).toBeNull();
+ });
+});
diff --git a/src/hooks/useLocationFilters.js b/src/hooks/useLocationFilters.js
index c3b4427c..2d6d8f4c 100644
--- a/src/hooks/useLocationFilters.js
+++ b/src/hooks/useLocationFilters.js
@@ -60,6 +60,7 @@ export const useLocationFilters = ({
criteria: initialFilter.criteria,
step: initialFilter.step,
fileName: initialFilter.fileName,
+ recordTypes: initialFilter.recordTypes
}),
});
},
diff --git a/src/utils/getRecordType.js b/src/utils/getRecordType.js
new file mode 100644
index 00000000..04079057
--- /dev/null
+++ b/src/utils/getRecordType.js
@@ -0,0 +1,9 @@
+import { CAPABILITIES, RECORD_TYPES } from '../constants';
+
+export const getRecordType = (capability) => {
+ if (Object.hasOwn(CAPABILITIES, capability)) {
+ return RECORD_TYPES[CAPABILITIES[capability]];
+ } else {
+ return null;
+ }
+};
diff --git a/src/utils/getRecordType.test.js b/src/utils/getRecordType.test.js
new file mode 100644
index 00000000..1b587111
--- /dev/null
+++ b/src/utils/getRecordType.test.js
@@ -0,0 +1,18 @@
+import { getRecordType } from './getRecordType';
+import { CAPABILITIES, RECORD_TYPES } from '../constants';
+
+describe('getRecordType', () => {
+ it('should return the correct record type for a valid capability', () => {
+ // Test each capability
+ Object.keys(CAPABILITIES).forEach((capability) => {
+ const result = getRecordType(capability);
+ expect(result).toEqual(RECORD_TYPES[CAPABILITIES[capability]]);
+ });
+ });
+
+ it('should return null for an invalid capability', () => {
+ const invalidCapability = 'INVALID_CAPABILITY';
+ const result = getRecordType(invalidCapability);
+ expect(result).toBeNull();
+ });
+});
diff --git a/test/jest/__mock__/stripesCore.mock.js b/test/jest/__mock__/stripesCore.mock.js
index a11e8cef..13f06292 100644
--- a/test/jest/__mock__/stripesCore.mock.js
+++ b/test/jest/__mock__/stripesCore.mock.js
@@ -1,14 +1,11 @@
-import React from 'react';
-import { noop } from 'lodash';
-
export const buildStripes = (otherProperties = {}) => ({
actionNames: [],
clone: buildStripes,
- connect: component => component,
+ connect: Comp => Comp,
config: {},
currency: 'USD',
- hasInterface: () => true,
- hasPerm: jest.fn(() => true),
+ hasInterface: jest.fn().mockReturnValue(true),
+ hasPerm: jest.fn().mockReturnValue(true),
locale: 'en-US',
logger: {
log: () => { },
@@ -18,17 +15,17 @@ export const buildStripes = (otherProperties = {}) => ({
url: 'https://folio-testing-okapi.dev.folio.org',
},
plugins: {},
- setBindings: noop,
- setCurrency: noop,
- setLocale: noop,
- setSinglePlugin: noop,
- setTimezone: noop,
- setToken: noop,
+ setBindings: () => { },
+ setCurrency: () => { },
+ setLocale: () => { },
+ setSinglePlugin: () => { },
+ setTimezone: () => { },
+ setToken: () => { },
store: {
- getState: noop,
- dispatch: noop,
- subscribe: noop,
- replaceReducer: noop,
+ getState: () => { },
+ dispatch: () => { },
+ subscribe: () => { },
+ replaceReducer: () => { },
},
timezone: 'UTC',
user: {
@@ -36,17 +33,19 @@ export const buildStripes = (otherProperties = {}) => ({
user: {
id: 'b1add99d-530b-5912-94f3-4091b4d87e2c',
username: 'diku_admin',
+ consortium: {
+ centralTenantId: 'consortia',
+ },
},
},
withOkapi: true,
...otherProperties,
});
-jest.mock('@folio/stripes/core', () => {
- const STRIPES = buildStripes();
+const STRIPES = buildStripes();
- // eslint-disable-next-line react/prop-types
- const stripesConnect = Component => ({ mutator, resources, stripes, ...rest }) => {
+const mockStripesCore = {
+ stripesConnect: Component => ({ mutator, resources, stripes, ...rest }) => {
const fakeMutator = mutator || Object.keys(Component.manifest).reduce((acc, mutatorName) => {
const returnValue = Component.manifest[mutatorName].records ? [] : {};
@@ -56,6 +55,8 @@ jest.mock('@folio/stripes/core', () => {
POST: jest.fn().mockReturnValue(Promise.resolve()),
DELETE: jest.fn().mockReturnValue(Promise.resolve()),
reset: jest.fn(),
+ update: jest.fn(),
+ replace: jest.fn(),
};
return acc;
@@ -71,44 +72,43 @@ jest.mock('@folio/stripes/core', () => {
const fakeStripes = stripes || STRIPES;
- // eslint-disable-next-line react/prop-types
- return (
-
- );
- };
-
- const withStripes = Component => ({ stripes, ...rest }) => {
+ return ;
+ },
+
+ useOkapiKy: jest.fn(),
+
+ useStripes: () => STRIPES,
+
+ withStripes: Component => ({ stripes, ...rest }) => {
const fakeStripes = stripes || STRIPES;
- return (
-
- );
- };
-
- const useStripes = () => STRIPES;
-
- // eslint-disable-next-line react/jsx-no-useless-fragment
- const IfPermission = props => <>{props.children}>;
-
- // eslint-disable-next-line react/jsx-no-useless-fragment
- const AppContextMenu = props => <>{props.children()}>;
-
- STRIPES.connect = stripesConnect;
-
- return {
- ...jest.requireActual('@folio/stripes/core'),
- stripesConnect,
- withStripes,
- IfPermission,
- AppContextMenu,
- useStripes,
- };
-}, { virtual: true });
+ return ;
+ },
+
+ // eslint-disable-next-line react/prop-types
+ Pluggable: props => <>
+
+ >,
+
+ // eslint-disable-next-line react/prop-types
+ IfPermission: jest.fn(props => <>{props.children}>),
+
+ // eslint-disable-next-line react/prop-types
+ IfInterface: jest.fn(props => <>{props.children}>),
+
+ useNamespace: () => ['@folio/inventory'],
+
+ TitleManager: ({ children }) => <>{children}>,
+
+ checkIfUserInMemberTenant: () => true,
+};
+
+jest.mock('@folio/stripes/core', () => ({
+ ...jest.requireActual('@folio/stripes/core'),
+ ...mockStripesCore
+}), { virtual: true });
+
+jest.mock('@folio/stripes-core', () => ({
+ ...jest.requireActual('@folio/stripes-core'),
+ ...mockStripesCore
+}), { virtual: true });