diff --git a/CHANGELOG.md b/CHANGELOG.md
index 75ab14ac..afe79524 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,22 @@
# Change history for ui-requests
-## 11.0.0 IN PROGRESS
+## 11.0.0 (IN PROGRESS)
+* Use settings/entries endpoint to get settings information. Refs UIREQ-1062.
+* Requests app.: Editing requests (ECS with mod-tlr enabled). Refs UIREQ-1088.
+* Requests app.: Cancelling request (ECS with mod-tlr enabled). Refs UIREQ-1090.
+* Requests app.: Reorder request queue (ECS with mod-tlr enabled). Refs UIREQ-1098.
+* Requests app.: Moving request (ECS with mod-tlr enabled). Refs UIREQ-1100.
+* Hide Action menu on secondary requests (ECS + mod-tlr). Refs UIREQ-1105.
+* *BREAKING* Use `circulation/items-by-instance` endpoint to get item and instance information. Refs UIREQ-1091.
+* Hide Duplicate and Move action buttons in ECS env with mod-tlr enabled. Refs UIREQ-1127, UIREQ-1125.
+* Update permissions set to be able to get item/instance information. Refs UIREQ-1148.
+* *BREAKING* Migrate to new endpoints to get request types and to create a new request. Refs UIREQ-1113.
+* Use `instanceId` param for ILR from items response. Refs UIREQ-1149.
+* Send `holdingsRecordId` param for Item level requests. Refs UIREQ-1167.
+* Add `tlr.settings.get` permission. Refs UIREQ-1169.
+* Add `mod-settings.global.read.circulation` permission. Refs UIREQ-1170.
+* Add `mod-settings.entries.collection.get` permission. Refs UIREQ-1177.
+* *BREAKING* Migrate to new `mod-circulation-bff` endpoints. Refs UIREQ-1134.
## [10.0.1] (https://github.com/folio-org/ui-requests/tree/v10.0.1) (2024-11-13)
[Full Changelog](https://github.com/folio-org/ui-requests/compare/v10.0.0...v10.0.1)
diff --git a/package.json b/package.json
index fee08e1b..1bd5cbba 100644
--- a/package.json
+++ b/package.json
@@ -31,7 +31,8 @@
"request-storage": "6.0",
"pick-slips": "0.1",
"search-slips": "0.1",
- "automated-patron-blocks": "0.1"
+ "automated-patron-blocks": "0.1",
+ "circulation-bff-requests": "1.0"
},
"permissionSets": [
{
@@ -57,7 +58,7 @@
"subPermissions": [
"circulation.requests.queue.collection.get",
"circulation.requests.queue.reorder.collection.post",
- "circulation.requests.allowed-service-points.get",
+ "circulation-bff.requests.allowed-service-points.get",
"circulation.rules.request-policy.get"
]
},
@@ -80,7 +81,6 @@
"subPermissions": [
"module.requests.enabled",
"circulation.loans.collection.get",
- "circulation.settings.collection.get",
"circulation.settings.item.get",
"circulation.requests.collection.get",
"circulation.requests.item.get",
@@ -108,7 +108,11 @@
"inventory-storage.instances.item.get",
"inventory-storage.instances.collection.get",
"manualblocks.collection.get",
- "circulation.requests.hold-shelf-clearance-report.get"
+ "circulation.requests.hold-shelf-clearance-report.get",
+ "circulation.settings.collection.get",
+ "tlr.settings.get",
+ "mod-settings.global.read.circulation",
+ "mod-settings.entries.collection.get"
],
"visible": true
},
@@ -118,15 +122,16 @@
"subPermissions": [
"ui-requests.view",
"automated-patron-blocks.collection.get",
- "circulation.requests.item.post",
- "circulation.requests.allowed-service-points.get",
+ "circulation-bff.requests.allowed-service-points.get",
"circulation-storage.requests.item.post",
"circulation-storage.request-preferences.collection.get",
"circulation-storage.staff-slips.collection.get",
"circulation.pick-slips.get",
"circulation.search-slips.get",
"circulation.print-events-entry.item.post",
- "inventory-storage.locations.item.get"
+ "inventory-storage.locations.item.get",
+ "circulation-bff.requests.search-instances.get",
+ "circulation-bff.requests.post"
],
"visible": true
},
@@ -138,7 +143,7 @@
"circulation.pick-slips.get",
"circulation.search-slips.get",
"circulation.requests.item.put",
- "circulation.requests.allowed-service-points.get",
+ "circulation-bff.requests.allowed-service-points.get",
"circulation.print-events-entry.item.post",
"circulation-storage.staff-slips.collection.get",
"circulation-storage.requests.collection.delete",
diff --git a/src/ItemDetail.test.js b/src/ItemDetail.test.js
index 7199b870..da740f54 100644
--- a/src/ItemDetail.test.js
+++ b/src/ItemDetail.test.js
@@ -9,6 +9,7 @@ import ItemDetail from './ItemDetail';
import { INVALID_REQUEST_HARDCODED_ID } from './constants';
jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
Link: jest.fn(({ to, children }) => {children}),
}));
diff --git a/src/ItemsDialog.js b/src/ItemsDialog.js
index 77d3e3d3..1dd77754 100644
--- a/src/ItemsDialog.js
+++ b/src/ItemsDialog.js
@@ -86,28 +86,17 @@ const ItemsDialog = ({
const [items, setItems] = useState([]);
const { formatMessage } = useIntl();
- const fetchHoldings = () => {
- const query = `instanceId==${instanceId}`;
- mutator.holdings.reset();
+ const fetchItems = () => {
+ const query = `id==${instanceId}`;
- return mutator.holdings.GET({ params: { query, limit: MAX_RECORDS } });
- };
-
- const fetchItems = async (holdings) => {
- const chunkedItems = chunk(holdings, CHUNK_SIZE);
- const data = [];
-
- for (const itemChunk of chunkedItems) {
- const query = itemChunk.map(i => `holdingsRecordId==${i.id}`).join(' or ');
-
- mutator.items.reset();
- // eslint-disable-next-line no-await-in-loop
- const result = await mutator.items.GET({ params: { query, limit: MAX_RECORDS } });
-
- data.push(...result);
- }
+ mutator.items.reset();
- return data;
+ return mutator.items.GET({
+ params: {
+ query,
+ limit: MAX_RECORDS,
+ },
+ });
};
const fetchRequests = async (itemsList) => {
@@ -136,8 +125,7 @@ const ItemsDialog = ({
const getItems = async () => {
setAreItemsBeingLoaded(true);
- const holdings = await fetchHoldings();
- let itemsList = await fetchItems(holdings);
+ let itemsList = await fetchItems();
if (skippedItemId) {
itemsList = itemsList.filter(item => requestableItemStatuses.includes(item.status?.name));
@@ -229,17 +217,10 @@ const ItemsDialog = ({
};
ItemsDialog.manifest = {
- holdings: {
- type: 'okapi',
- records: 'holdingsRecords',
- path: 'holdings-storage/holdings',
- accumulate: true,
- fetch: false,
- },
items: {
type: 'okapi',
records: 'items',
- path: 'inventory/items',
+ path: 'circulation-bff/requests/search-instances',
accumulate: true,
fetch: false,
},
@@ -265,10 +246,6 @@ ItemsDialog.propTypes = {
skippedItemId: PropTypes.string,
onRowClick: PropTypes.func,
mutator: PropTypes.shape({
- holdings: PropTypes.shape({
- GET: PropTypes.func.isRequired,
- reset: PropTypes.func.isRequired,
- }).isRequired,
items: PropTypes.shape({
GET: PropTypes.func.isRequired,
reset: PropTypes.func.isRequired,
diff --git a/src/ItemsDialog.test.js b/src/ItemsDialog.test.js
index 6cabf6cd..dd8be303 100644
--- a/src/ItemsDialog.test.js
+++ b/src/ItemsDialog.test.js
@@ -45,20 +45,6 @@ describe('ItemsDialog', () => {
const testTitle = 'testTitle';
const testInstanceId = 'testInstanceId';
const testMutator = {
- holdings: {
- GET: jest.fn(() => (new Promise((resolve) => {
- setTimeout(() => {
- resolve(
- [{
- id: '1',
- }, {
- id: '2',
- }]
- );
- });
- }))),
- reset: jest.fn(),
- },
items: {
GET: jest.fn(() => (new Promise((resolve) => {
setTimeout(() => {
@@ -120,8 +106,6 @@ describe('ItemsDialog', () => {
Paneset.mockClear();
Loading.mockClear();
onClose.mockClear();
- testMutator.holdings.GET.mockClear();
- testMutator.holdings.reset.mockClear();
testMutator.items.GET.mockClear();
testMutator.items.reset.mockClear();
testMutator.requests.GET.mockClear();
@@ -272,7 +256,6 @@ describe('ItemsDialog', () => {
});
const allItemStatuses = Object.values(itemStatuses);
const newMutator = {
- ...testMutator,
items: {
GET: jest.fn(() => (new Promise((resolve) => {
setTimeout(() => {
diff --git a/src/MoveRequestManager.js b/src/MoveRequestManager.js
index 027b5add..c73c0db6 100644
--- a/src/MoveRequestManager.js
+++ b/src/MoveRequestManager.js
@@ -156,7 +156,7 @@ class MoveRequestManager extends React.Component {
token: stripes.store.getState().okapi.token,
})
};
- const url = `${stripes.okapi.url}/circulation/requests/allowed-service-points?requestId=${request.id}&itemId=${selectedItem.id}&operation=${REQUEST_OPERATIONS.MOVE}`;
+ const url = `${stripes.okapi.url}/circulation-bff/requests/allowed-service-points?requestId=${request.id}&itemId=${selectedItem.id}&operation=${REQUEST_OPERATIONS.MOVE}`;
this.setState({
isRequestTypesLoading: true,
diff --git a/src/MoveRequestManager.test.js b/src/MoveRequestManager.test.js
index 0629d5ff..7aa4518a 100644
--- a/src/MoveRequestManager.test.js
+++ b/src/MoveRequestManager.test.js
@@ -188,7 +188,7 @@ describe('MoveRequestManager', () => {
});
it('should trigger fetch with correct argument', () => {
- const expectedUrl = `${basicProps.stripes.okapi.url}/circulation/requests/allowed-service-points?requestId=${basicProps.request.id}&itemId=${selectedItem.id}&operation=${REQUEST_OPERATIONS.MOVE}`;
+ const expectedUrl = `${basicProps.stripes.okapi.url}/circulation-bff/requests/allowed-service-points?requestId=${basicProps.request.id}&itemId=${selectedItem.id}&operation=${REQUEST_OPERATIONS.MOVE}`;
expect(global.fetch).toHaveBeenCalledWith(expectedUrl, {});
});
diff --git a/src/PositionLink.test.js b/src/PositionLink.test.js
index 8ab209d4..41e97d4e 100644
--- a/src/PositionLink.test.js
+++ b/src/PositionLink.test.js
@@ -10,6 +10,7 @@ import {
import PositionLink from './PositionLink';
jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
Link: jest.fn(({ to, children }) => {children}),
}));
diff --git a/src/RequestForm.js b/src/RequestForm.js
index a4e9d8d2..aacc5e43 100644
--- a/src/RequestForm.js
+++ b/src/RequestForm.js
@@ -18,7 +18,6 @@ import {
defer,
pick,
isBoolean,
- isNil,
} from 'lodash';
import {
@@ -144,7 +143,6 @@ class RequestForm extends React.Component {
values: PropTypes.object.isRequired,
form: PropTypes.object.isRequired,
blocked: PropTypes.bool.isRequired,
- instanceId: PropTypes.string.isRequired,
isPatronBlocksOverridden: PropTypes.bool.isRequired,
onSubmit: PropTypes.func,
parentMutator: PropTypes.shape({
@@ -161,7 +159,6 @@ class RequestForm extends React.Component {
onSetSelectedInstance: PropTypes.func.isRequired,
onSetBlocked: PropTypes.func.isRequired,
onSetIsPatronBlocksOverridden: PropTypes.func.isRequired,
- onSetInstanceId: PropTypes.func.isRequired,
};
static defaultProps = {
@@ -718,22 +715,21 @@ class RequestForm extends React.Component {
findItemRelatedResources(item) {
const {
findResource,
- onSetInstanceId,
} = this.props;
- if (!item) return null;
+
+ if (!item) {
+ return null;
+ }
return Promise.all(
[
findResource('loan', item.id),
findResource('requestsForItem', item.id),
- findResource(RESOURCE_TYPES.HOLDING, item.holdingsRecordId),
],
).then((results) => {
const selectedLoan = results[0]?.loans?.[0];
const itemRequestCount = results[1]?.requests?.length;
- const holdingsRecord = results[2]?.holdingsRecords?.[0];
- onSetInstanceId(holdingsRecord?.instanceId);
this.setState({
itemRequestCount,
selectedLoan,
@@ -773,7 +769,7 @@ class RequestForm extends React.Component {
if (isValidation) {
return findResource(RESOURCE_TYPES.ITEM, value, key)
.then((result) => {
- return result.totalRecords;
+ return result?.items?.length;
})
.finally(() => {
this.setState({ isItemOrInstanceLoading: false });
@@ -788,7 +784,7 @@ class RequestForm extends React.Component {
.then((result) => {
this.setItemIdRequest(key, isBarcodeRequired);
- if (!result || result.totalRecords === 0) {
+ if (!result || result?.items?.length === 0) {
this.setState({
isItemOrInstanceLoading: false,
});
@@ -796,21 +792,26 @@ class RequestForm extends React.Component {
return null;
}
- const item = result.items[0];
+ const foundItem = result.items?.find(item => item[key] === value);
- form.change(REQUEST_FORM_FIELD_NAMES.ITEM_ID, item.id);
- form.change(REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE, item.barcode);
+ form.change(REQUEST_FORM_FIELD_NAMES.ITEM_ID, foundItem.id);
+ form.change(REQUEST_FORM_FIELD_NAMES.ITEM_BARCODE, foundItem.barcode);
resetFieldState(form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE);
// Setting state here is redundant with what follows, but it lets us
// display the matched item as quickly as possible, without waiting for
// the slow loan and request lookups
- onSetSelectedItem(item);
+ onSetSelectedItem(foundItem);
this.setState({
isItemOrInstanceLoading: false,
});
- return item;
+ return foundItem;
+ })
+ .catch(() => {
+ onSetSelectedItem(null);
+
+ return null;
})
.then(item => {
if (item && selectedUser?.id) {
@@ -825,7 +826,7 @@ class RequestForm extends React.Component {
}
findInstanceRelatedResources(instance) {
- if (!instance) {
+ if (!instance?.id) {
return null;
}
@@ -841,7 +842,7 @@ class RequestForm extends React.Component {
});
}
- findInstance = async (instanceId, holdingsRecordId, isValidation = false) => {
+ findInstance = async (instanceId, isValidation = false) => {
const {
findResource,
form,
@@ -854,14 +855,10 @@ class RequestForm extends React.Component {
isItemOrInstanceLoading: true,
});
- const resultInstanceId = isNil(instanceId)
- ? await findResource(RESOURCE_TYPES.HOLDING, holdingsRecordId).then((holding) => holding.holdingsRecords[0].instanceId)
- : instanceId;
-
if (isValidation) {
- return findResource(RESOURCE_TYPES.INSTANCE, resultInstanceId)
+ return findResource(RESOURCE_TYPES.INSTANCE, instanceId)
.then((result) => {
- return result.totalRecords;
+ return Boolean(result?.id);
})
.finally(() => {
this.setState({ isItemOrInstanceLoading: false });
@@ -872,9 +869,9 @@ class RequestForm extends React.Component {
isRequestTypesReceived: false,
});
- return findResource(RESOURCE_TYPES.INSTANCE, resultInstanceId)
- .then((result) => {
- if (!result || result.totalRecords === 0) {
+ return findResource(RESOURCE_TYPES.INSTANCE, instanceId)
+ .then((instance) => {
+ if (!instance?.id) {
this.setState({
isItemOrInstanceLoading: false,
});
@@ -882,8 +879,6 @@ class RequestForm extends React.Component {
return null;
}
- const instance = result.instances[0];
-
form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_ID, instance.id);
form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instance.hrid);
resetFieldState(form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE);
@@ -895,6 +890,11 @@ class RequestForm extends React.Component {
return instance;
})
+ .catch(() => {
+ onSetSelectedInstance(null);
+
+ return null;
+ })
.then(instance => {
if (instance && selectedUser?.id) {
const requester = getRequester(proxy, selectedUser);
@@ -1009,15 +1009,16 @@ class RequestForm extends React.Component {
form.change(REQUEST_FORM_FIELD_NAMES.INSTANCE_ID, null);
if (isCreateTlr) {
- onSetSelectedItem(undefined);
this.setState({
requestTypes: {},
isRequestTypesReceived: false,
});
if (selectedItem) {
- this.findInstance(null, selectedItem.holdingsRecordId);
+ this.findInstance(selectedItem.instanceId);
}
+
+ onSetSelectedItem(undefined);
} else if (selectedInstance) {
form.change(REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE, DEFAULT_REQUEST_TYPE_VALUE);
resetFieldState(form, REQUEST_FORM_FIELD_NAMES.REQUEST_TYPE);
@@ -1099,7 +1100,6 @@ class RequestForm extends React.Component {
selectedUser,
selectedInstance,
isPatronBlocksOverridden,
- instanceId,
blocked,
values,
onCancel,
@@ -1289,7 +1289,6 @@ class RequestForm extends React.Component {
onSetSelectedInstance={onSetSelectedInstance}
isLoading={isItemOrInstanceLoading}
instanceRequestCount={instanceRequestCount}
- instanceId={instanceId}
/>
@@ -1311,7 +1310,6 @@ class RequestForm extends React.Component {
onSetSelectedItem={onSetSelectedItem}
values={values}
itemRequestCount={itemRequestCount}
- instanceId={instanceId}
selectedLoan={selectedLoan}
isLoading={isItemOrInstanceLoading}
/>
diff --git a/src/RequestForm.test.js b/src/RequestForm.test.js
index 2a723aba..ace72ad8 100644
--- a/src/RequestForm.test.js
+++ b/src/RequestForm.test.js
@@ -19,6 +19,8 @@ import RequestForm, {
} from './RequestForm';
import FulfilmentPreference from './components/FulfilmentPreference';
import RequesterInformation from './components/RequesterInformation';
+import ItemInformation from './components/ItemInformation';
+import InstanceInformation from './components/InstanceInformation';
import {
REQUEST_LEVEL_TYPES,
@@ -52,6 +54,8 @@ const testIds = {
closePatronModalButton: 'closePatronModalButton',
itemDialogCloseButton: 'itemDialogCloseButton',
itemDialogRow: 'itemDialogRow',
+ itemEnterButton: 'itemEnterButton',
+ instanceEnterButton: 'instanceEnterButton',
};
const fieldValue = 'value';
const idResourceType = 'id';
@@ -214,7 +218,6 @@ describe('RequestForm', () => {
onSetSelectedInstance: jest.fn(),
onSetSelectedItem: jest.fn(),
onSetSelectedUser: jest.fn(),
- onSetInstanceId: jest.fn(),
onSetIsPatronBlocksOverridden: jest.fn(),
onSetBlocked: jest.fn(),
onShowErrorModal: jest.fn(),
@@ -311,22 +314,14 @@ describe('RequestForm', () => {
});
describe('Initial render', () => {
- const holding = {
- holdingsRecords: [
- {
- instanceId: 'instanceId',
- }
- ],
- };
const selectedItem = {
+ instanceId: 'instanceId',
holdingsRecordId: 'holdingsRecordId',
};
let findResource;
beforeEach(() => {
- findResource = jest.fn()
- .mockResolvedValueOnce(holding)
- .mockResolvedValueOnce(null);
+ findResource = jest.fn().mockResolvedValueOnce(null);
const props = {
...basicProps,
@@ -366,8 +361,8 @@ describe('RequestForm', () => {
expect(basicProps.onSetSelectedItem).toHaveBeenCalledWith(undefined);
});
- it('should get instance id if it is not provided', () => {
- const expectedArgs = [RESOURCE_TYPES.HOLDING, selectedItem.holdingsRecordId];
+ it('should get instance information', () => {
+ const expectedArgs = [RESOURCE_TYPES.INSTANCE, selectedItem.instanceId];
const tlrCheckbox = screen.getByTestId(testIds.tlrCheckbox);
fireEvent.click(tlrCheckbox);
@@ -1235,12 +1230,12 @@ describe('RequestForm', () => {
},
};
const itemResult = {
- totalRecords: 1,
items: [
{
id: 'itemId',
barcode: initialItemBarcode,
holdingsRecordId: 'holdingsRecordId',
+ instanceId,
}
],
};
@@ -1262,13 +1257,6 @@ describe('RequestForm', () => {
const itemRequestsResult = {
requests: [],
};
- const holdingsRecordResult = {
- holdingsRecords: [
- {
- instanceId,
- }
- ],
- };
let findResource;
beforeEach(() => {
@@ -1278,7 +1266,6 @@ describe('RequestForm', () => {
.mockResolvedValueOnce(requestTypesResult)
.mockResolvedValueOnce(loanResult)
.mockResolvedValueOnce(itemRequestsResult)
- .mockResolvedValueOnce(holdingsRecordResult)
.mockResolvedValue({});
const props = {
@@ -1347,19 +1334,6 @@ describe('RequestForm', () => {
expect(findResource).toHaveBeenCalledWith(...expectedArgs);
});
- it('should get information about holdings', () => {
- const expectedArgs = [
- RESOURCE_TYPES.HOLDING,
- itemResult.items[0].holdingsRecordId
- ];
-
- expect(findResource).toHaveBeenCalledWith(...expectedArgs);
- });
-
- it('should set instance id', () => {
- expect(basicProps.onSetInstanceId).toHaveBeenCalledWith(holdingsRecordResult.holdingsRecords[0].instanceId);
- });
-
it('should handle item barcode field change', () => {
const expectedArgs = [RESOURCE_TYPES.ITEM, fieldValue, RESOURCE_KEYS.id];
const itemField = screen.getByTestId(testIds.itemField);
@@ -1381,7 +1355,6 @@ describe('RequestForm', () => {
describe('When item is not found', () => {
const itemResult = {
- totalRecords: 0,
items: [],
};
let findResource;
@@ -1430,6 +1403,47 @@ describe('RequestForm', () => {
expect(findResource).not.toHaveBeenCalledWith(...expectedArgs);
});
});
+
+ describe('When error during item finding', () => {
+ beforeEach(() => {
+ const props = {
+ ...basicProps,
+ request: undefined,
+ findResource: jest.fn()
+ .mockRejectedValue({}),
+ };
+
+ ItemInformation.mockImplementationOnce(({
+ findItem,
+ }) => {
+ const handleClick = () => {
+ findItem(idResourceType, 'id', false);
+ };
+
+ return (
+
+ );
+ });
+
+ renderComponent(props);
+ });
+
+ it('should reset item information', async () => {
+ const itemField = screen.getByTestId(testIds.itemEnterButton);
+
+ fireEvent.click(itemField);
+
+ await waitFor(() => {
+ expect(basicProps.onSetSelectedItem).toHaveBeenCalledWith(null);
+ });
+ });
+ });
});
describe('Component updating', () => {
@@ -1479,7 +1493,6 @@ describe('RequestForm', () => {
const initialItemId = 'itemId';
const updatedItemId = 'updatedItemId';
const itemResult = {
- totalRecords: 0,
items: [],
};
@@ -1564,13 +1577,8 @@ describe('RequestForm', () => {
},
};
const instanceResult = {
- totalRecords: 1,
- instances: [
- {
- id: initialInstanceId,
- hrid: 'hrid',
- }
- ],
+ id: initialInstanceId,
+ hrid: 'hrid',
};
const requestTypesResult = {
'Page': [
@@ -1623,8 +1631,8 @@ describe('RequestForm', () => {
it('should set instance information', () => {
const expectedArgs = [
- [REQUEST_FORM_FIELD_NAMES.INSTANCE_ID, instanceResult.instances[0].id],
- [REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instanceResult.instances[0].hrid]
+ [REQUEST_FORM_FIELD_NAMES.INSTANCE_ID, instanceResult.id],
+ [REQUEST_FORM_FIELD_NAMES.INSTANCE_HRID, instanceResult.hrid]
];
expectedArgs.forEach(args => {
@@ -1645,14 +1653,14 @@ describe('RequestForm', () => {
it('should get information about open instance requests', () => {
const expectedArgs = [
'requestsForInstance',
- instanceResult.instances[0].id
+ instanceResult.id
];
expect(findResource).toHaveBeenCalledWith(...expectedArgs);
});
it('should set selected instance', () => {
- expect(basicProps.onSetSelectedInstance).toHaveBeenCalledWith(instanceResult.instances[0]);
+ expect(basicProps.onSetSelectedInstance).toHaveBeenCalledWith(instanceResult);
});
it('should handle instance id field change', () => {
@@ -1675,10 +1683,7 @@ describe('RequestForm', () => {
});
describe('When instance is not found', () => {
- const instanceResult = {
- totalRecords: 0,
- instances: [],
- };
+ const instanceResult = {};
let findResource;
beforeEach(() => {
@@ -1716,6 +1721,51 @@ describe('RequestForm', () => {
expect(resetFieldState).not.toHaveBeenCalledWith(...expectedArgs);
});
});
+
+ describe('When error during instance finding', () => {
+ beforeEach(() => {
+ const props = {
+ ...basicProps,
+ request: undefined,
+ values: {
+ ...basicProps.values,
+ createTitleLevelRequest: true,
+ },
+ findResource: jest.fn()
+ .mockRejectedValue({}),
+ };
+
+ InstanceInformation.mockImplementationOnce(({
+ findInstance,
+ }) => {
+ const handleClick = () => {
+ findInstance('id', false);
+ };
+
+ return (
+
+ );
+ });
+
+ renderComponent(props);
+ });
+
+ it('should reset instance information', async () => {
+ const itemField = screen.getByTestId(testIds.instanceEnterButton);
+
+ fireEvent.click(itemField);
+
+ await waitFor(() => {
+ expect(basicProps.onSetSelectedInstance).toHaveBeenCalledWith(null);
+ });
+ });
+ });
});
describe('Component updating', () => {
diff --git a/src/RequestFormContainer.js b/src/RequestFormContainer.js
index 0c062603..c2810738 100644
--- a/src/RequestFormContainer.js
+++ b/src/RequestFormContainer.js
@@ -39,7 +39,6 @@ const RequestFormContainer = ({
const [selectedUser, setSelectedUser] = useState({ ...requester, id: requesterId });
const [selectedInstance, setSelectedInstance] = useState(request?.instance);
const [isPatronBlocksOverridden, setIsPatronBlocksOverridden] = useState(false);
- const [instanceId, setInstanceId] = useState('');
const [blocked, setBlocked] = useState(false);
const setItem = (optedItem) => {
@@ -62,10 +61,6 @@ const RequestFormContainer = ({
setIsPatronBlocksOverridden(value);
};
- const setStateInstanceId = (id) => {
- setInstanceId(id);
- };
-
const getPatronManualBlocks = (resources) => {
return (resources?.patronBlocks?.records || [])
.filter(b => b.requests === true)
@@ -142,7 +137,7 @@ const RequestFormContainer = ({
};
}
- requestData.instanceId = request?.instanceId || instanceId || selectedInstance?.id;
+ requestData.instanceId = request?.instanceId || selectedInstance?.id || selectedItem?.instanceId;
requestData.requestLevel = request?.requestLevel || getRequestLevelValue(requestData.createTitleLevelRequest);
if (requestData.requestLevel === REQUEST_LEVEL_TYPES.ITEM) {
@@ -179,7 +174,6 @@ const RequestFormContainer = ({
selectedUser={selectedUser}
selectedInstance={selectedInstance}
isPatronBlocksOverridden={isPatronBlocksOverridden}
- instanceId={instanceId}
onGetPatronManualBlocks={getPatronManualBlocks}
onGetAutomatedPatronBlocks={getAutomatedPatronBlocks}
onSetBlocked={setIsBlocked}
@@ -187,7 +181,6 @@ const RequestFormContainer = ({
onSetSelectedUser={setUser}
onSetSelectedInstance={setInstance}
onSetIsPatronBlocksOverridden={setStateIsPatronBlocksOverridden}
- onSetInstanceId={setStateInstanceId}
onSubmit={handleSubmit}
/>
);
diff --git a/src/RequestFormContainer.test.js b/src/RequestFormContainer.test.js
index 64af25fb..e0c4c3f5 100644
--- a/src/RequestFormContainer.test.js
+++ b/src/RequestFormContainer.test.js
@@ -55,7 +55,6 @@ describe('RequestFormContainer', () => {
},
selectedInstance: defaultProps.request.instance,
isPatronBlocksOverridden: false,
- instanceId: '',
onGetPatronManualBlocks: expect.any(Function),
onGetAutomatedPatronBlocks: expect.any(Function),
onSetBlocked: expect.any(Function),
@@ -63,7 +62,6 @@ describe('RequestFormContainer', () => {
onSetSelectedUser: expect.any(Function),
onSetSelectedInstance: expect.any(Function),
onSetIsPatronBlocksOverridden: expect.any(Function),
- onSetInstanceId: expect.any(Function),
onSubmit: expect.any(Function),
};
@@ -100,6 +98,10 @@ describe('RequestFormContainer', () => {
};
const props = {
...defaultProps,
+ request: {
+ ...defaultProps.request,
+ instanceId: null,
+ },
itemId: 'itemId',
item: {
id: 'id',
@@ -107,6 +109,7 @@ describe('RequestFormContainer', () => {
};
const selectedItem = {
holdingsRecordId: 'holdingsRecordId',
+ instanceId: 'instanceId',
};
const selectItemLabel = 'Select Item';
@@ -144,7 +147,7 @@ describe('RequestFormContainer', () => {
const expectedArg = {
holdingsRecordId: selectedItem.holdingsRecordId,
fulfillmentPreference: fulfillmentTypeMap.HOLD_SHELF,
- instanceId: defaultProps.request.instanceId,
+ instanceId: selectedItem.instanceId,
requestLevel: REQUEST_LEVEL_TYPES.ITEM,
pickupServicePointId: submitData.pickupServicePointId,
item: submitData.item,
diff --git a/src/ViewRequest.js b/src/ViewRequest.js
index 8d00cf7b..86324a87 100644
--- a/src/ViewRequest.js
+++ b/src/ViewRequest.js
@@ -66,10 +66,22 @@ import {
isVirtualItem,
isVirtualPatron,
getRequestErrorMessage,
+ isMultiDataTenant,
} from './utils';
import urls from './routes/urls';
const CREATE_SUCCESS = 'CREATE_SUCCESS';
+const ECS_REQUEST_PHASE = {
+ PRIMARY: 'Primary',
+ SECONDARY: 'Secondary',
+};
+
+export const isAnyActionButtonVisible = (visibilityConditions = []) => visibilityConditions.some(condition => condition);
+export const shouldHideMoveAndDuplicate = (stripes, isEcsTlrPrimaryRequest, isEcsTlrSettingReceived, isEcsTlrSettingEnabled) => {
+ return isEcsTlrPrimaryRequest ||
+ (isEcsTlrSettingReceived && isEcsTlrSettingEnabled) ||
+ (isMultiDataTenant(stripes) && !isEcsTlrSettingReceived);
+};
class ViewRequest extends React.Component {
static manifest = {
@@ -127,7 +139,9 @@ class ViewRequest extends React.Component {
other: PropTypes.shape({
totalRecords: PropTypes.number,
}),
- records: PropTypes.arrayOf(PropTypes.object),
+ records: PropTypes.arrayOf(PropTypes.shape({
+ ecsRequestPhase: PropTypes.object,
+ })),
}),
}),
query: PropTypes.object,
@@ -141,6 +155,8 @@ class ViewRequest extends React.Component {
intl: PropTypes.object,
tagsEnabled: PropTypes.bool,
match: PropTypes.object,
+ isEcsTlrSettingReceived: PropTypes.bool.isRequired,
+ isEcsTlrSettingEnabled: PropTypes.bool.isRequired,
};
static defaultProps = {
@@ -457,6 +473,8 @@ class ViewRequest extends React.Component {
stripes,
patronGroups,
optionLists: { cancellationReasons },
+ isEcsTlrSettingEnabled,
+ isEcsTlrSettingReceived,
} = this.props;
const {
isCancellingRequest,
@@ -468,7 +486,8 @@ class ViewRequest extends React.Component {
item,
requester
} = request;
-
+ const isEcsTlrPrimaryRequest = request?.ecsRequestPhase === ECS_REQUEST_PHASE.PRIMARY;
+ const isEcsTlrSecondaryRequest = request?.ecsRequestPhase === ECS_REQUEST_PHASE.SECONDARY;
const getPickupServicePointName = this.getPickupServicePointName(request);
const requestStatus = get(request, ['status'], '-');
const isRequestClosed = requestStatus.startsWith('Closed');
@@ -501,14 +520,15 @@ class ViewRequest extends React.Component {
?
: '-';
- const showActionMenu = stripes.hasPerm('ui-requests.create')
- || stripes.hasPerm('ui-requests.edit')
- || stripes.hasPerm('ui-requests.moveRequest.execute')
- || stripes.hasPerm('ui-requests.reorderQueue.execute') || !isDCBTransaction;
-
const actionMenu = ({ onToggle }) => {
+ if (isEcsTlrSecondaryRequest) {
+ return null;
+ }
+
+ const isMoveAndDuplicateHidden = shouldHideMoveAndDuplicate(stripes, isEcsTlrPrimaryRequest, isEcsTlrSettingReceived, isEcsTlrSettingEnabled);
+
if (isRequestClosed) {
- if (!isRequestValid || (requestLevel === REQUEST_LEVEL_TYPES.TITLE && !titleLevelRequestsFeatureEnabled) || isDCBTransaction) {
+ if (!isRequestValid || (requestLevel === REQUEST_LEVEL_TYPES.TITLE && !titleLevelRequestsFeatureEnabled) || isDCBTransaction || isMoveAndDuplicateHidden) {
return null;
}
@@ -530,11 +550,18 @@ class ViewRequest extends React.Component {
);
}
- return (
- <>
-
- {
- isRequestValid && !isDCBTransaction &&
+ const isValidNotDCBTransaction = isRequestValid && !isDCBTransaction;
+ const isEditButtonVisible = isValidNotDCBTransaction && stripes.hasPerm('ui-requests.edit');
+ const isCancelButtonVisible = stripes.hasPerm('ui-requests.edit');
+ const isDuplicateButtonVisible = isValidNotDCBTransaction && !isMoveAndDuplicateHidden && stripes.hasPerm('ui-requests.create');
+ const isMoveButtonVisible = item && isRequestNotFilled && isValidNotDCBTransaction && !isMoveAndDuplicateHidden && stripes.hasPerm('ui-requests.moveRequest.execute');
+ const isReorderQueueButtonVisible = isRequestOpen && isValidNotDCBTransaction && stripes.hasPerm('ui-requests.reorderQueue.execute');
+ const isActionMenuVisible = isAnyActionButtonVisible([isEditButtonVisible, isCancelButtonVisible, isDuplicateButtonVisible, isMoveButtonVisible, isReorderQueueButtonVisible]);
+
+ if (isActionMenuVisible) {
+ return (
+ <>
+ {isEditButtonVisible &&
}
-
-
- {
- isRequestValid && !isDCBTransaction &&
-
+ {isCancelButtonVisible &&
+
+ }
+ {isDuplicateButtonVisible &&
-
- }
- {item && isRequestNotFilled && isRequestValid && !isDCBTransaction &&
-
+ }
+ {isMoveButtonVisible &&
- }
- {isRequestOpen && isRequestValid && !isDCBTransaction &&
-
+ }
+ {isReorderQueueButtonVisible &&
- }
- >
- );
+ }
+ >
+ );
+ }
+
+ return null;
};
const referredRecordData = {
@@ -641,7 +667,7 @@ class ViewRequest extends React.Component {
paneTitle={}
lastMenu={this.renderDetailMenu(request)}
dismissible
- {... (showActionMenu ? { actionMenu } : {})}
+ actionMenu={actionMenu}
onClose={this.props.onClose}
>
{
};
const mockedConfig = {
records: [
- { value: '{"titleLevelRequestsFeatureEnabled":true}' },
+ {
+ value: {
+ titleLevelRequestsFeatureEnabled: true,
+ },
+ }
],
};
const defaultProps = {
@@ -114,6 +121,7 @@ describe('ViewRequest', () => {
},
stripes: {
hasPerm: jest.fn(() => true),
+ hasInterface: jest.fn(() => true),
connect: jest.fn((component) => component),
logger: {
log: jest.fn(),
@@ -124,6 +132,8 @@ describe('ViewRequest', () => {
id: 'testId',
},
},
+ isEcsTlrSettingEnabled: false,
+ isEcsTlrSettingReceived: true,
};
const defaultDCBLendingProps = {
...defaultProps,
@@ -195,7 +205,9 @@ describe('ViewRequest', () => {
describe('request is valid', () => {
describe('TLR in enabled', () => {
beforeAll(() => {
- mockedConfig.records[0].value = '{"titleLevelRequestsFeatureEnabled":true}';
+ mockedConfig.records[0].value = {
+ titleLevelRequestsFeatureEnabled: true,
+ };
});
it('should render "Duplicate" button', () => {
@@ -205,7 +217,9 @@ describe('ViewRequest', () => {
describe('TLR in disabled', () => {
beforeAll(() => {
- mockedConfig.records[0].value = '{"titleLevelRequestsFeatureEnabled":false}';
+ mockedConfig.records[0].value = {
+ titleLevelRequestsFeatureEnabled: false,
+ };
});
it('should not render "Duplicate" button', () => {
@@ -490,4 +504,40 @@ describe('ViewRequest', () => {
});
});
});
+
+ describe('isAnyActionButtonVisible', () => {
+ describe('When visibility conditions are provided', () => {
+ it('should return true', () => {
+ expect(isAnyActionButtonVisible([true, false])).toBe(true);
+ });
+ });
+
+ describe('When visibility conditions are not provided', () => {
+ it('should return false', () => {
+ expect(isAnyActionButtonVisible()).toBe(false);
+ });
+ });
+ });
+
+ describe('shouldHideMoveAndDuplicate', () => {
+ const stripes = {
+ hasInterface: () => true,
+ };
+
+ it('should return true for primary request', () => {
+ expect(shouldHideMoveAndDuplicate(stripes, true)).toBe(true);
+ });
+
+ it('should return true if ecs tlr enabled', () => {
+ expect(shouldHideMoveAndDuplicate(stripes, false, true, true)).toBe(true);
+ });
+
+ it('should return true if settings are not received', () => {
+ expect(shouldHideMoveAndDuplicate(stripes, false, false, true)).toBe(true);
+ });
+
+ it('should return false', () => {
+ expect(shouldHideMoveAndDuplicate(stripes, false, true, false)).toBe(false);
+ });
+ });
});
diff --git a/src/components/InstanceInformation/InstanceInformation.js b/src/components/InstanceInformation/InstanceInformation.js
index 018e1e22..745b0d81 100644
--- a/src/components/InstanceInformation/InstanceInformation.js
+++ b/src/components/InstanceInformation/InstanceInformation.js
@@ -37,7 +37,6 @@ class InstanceInformation extends Component {
values: PropTypes.object.isRequired,
onSetSelectedInstance: PropTypes.func.isRequired,
isLoading: PropTypes.bool.isRequired,
- instanceId: PropTypes.string.isRequired,
request: PropTypes.object,
instanceRequestCount: PropTypes.number,
selectedInstance: PropTypes.object,
@@ -68,7 +67,7 @@ class InstanceInformation extends Component {
if (instanceId && shouldValidateId) {
this.setState({ shouldValidateId: false });
- const instance = await findInstance(instanceId, null, true);
+ const instance = await findInstance(instanceId, true);
return !instance
?
@@ -159,7 +158,6 @@ class InstanceInformation extends Component {
values,
isLoading,
instanceRequestCount,
- instanceId,
} = this.props;
const {
isInstanceClicked,
@@ -247,7 +245,7 @@ class InstanceInformation extends Component {
{
isTitleInfoVisible &&
{
});
describe('when instance is selected', () => {
- it('should render "TitleInformation" with correct props', () => {
- render(
-
- );
-
- const expectedProps = {
- instanceId: basicProps.instanceId,
- titleLevelRequestsCount: basicProps.instanceRequestCount,
- title: basicProps.selectedInstance.title,
- contributors: basicProps.selectedInstance.contributors,
- publications: basicProps.selectedInstance.publication,
- editions: basicProps.selectedInstance.editions,
- identifiers: basicProps.selectedInstance.identifiers,
- };
-
- expect(TitleInformation).toHaveBeenCalledWith(expectedProps, {});
- });
-
it('should render "TitleInformation" with "request.instanceId"', () => {
const instanceId = 'instanceId';
const props = {
@@ -627,21 +607,13 @@ describe('InstanceInformation', () => {
});
it('should render "TitleInformation" with "selectedInstance.id"', () => {
- const selectedInstanceId = 'selectedInstanceId';
- const props = {
- ...basicProps,
- selectedInstance: {
- ...basicProps.selectedInstance,
- id: selectedInstanceId,
- },
- };
const expectedProps = {
- instanceId: selectedInstanceId,
+ instanceId: basicProps.selectedInstance.id,
};
render(
);
diff --git a/src/components/ItemInformation/ItemInformation.js b/src/components/ItemInformation/ItemInformation.js
index c6bf74bf..3b1e9f1a 100644
--- a/src/components/ItemInformation/ItemInformation.js
+++ b/src/components/ItemInformation/ItemInformation.js
@@ -35,7 +35,6 @@ class ItemInformation extends Component {
request: PropTypes.object.isRequired,
onSetSelectedItem: PropTypes.func.isRequired,
itemRequestCount: PropTypes.number.isRequired,
- instanceId: PropTypes.string.isRequired,
isLoading: PropTypes.bool.isRequired,
submitting: PropTypes.bool.isRequired,
isItemIdRequest: PropTypes.bool.isRequired,
@@ -162,7 +161,6 @@ class ItemInformation extends Component {
isLoading,
selectedItem,
request,
- instanceId,
selectedLoan,
itemRequestCount,
} = this.props;
@@ -235,7 +233,7 @@ class ItemInformation extends Component {
selectedItem &&
{
});
describe('when item is selected', () => {
+ const props = {
+ ...basicProps,
+ selectedItem: {
+ id: 'itemId',
+ instanceId: 'instanceId',
+ },
+ };
+
beforeEach(() => {
render(
);
});
@@ -557,7 +564,7 @@ describe('ItemInformation', () => {
it('should render "ItemDetail" with correct props', () => {
const expectedProps = {
request: basicProps.request,
- currentInstanceId: basicProps.instanceId,
+ currentInstanceId: basicProps.selectedItem.instanceId,
item: basicProps.selectedItem,
loan: basicProps.selectedLoan,
requestCount: basicProps.itemRequestCount,
diff --git a/src/constants.js b/src/constants.js
index c2ef1644..a4d7c6ae 100644
--- a/src/constants.js
+++ b/src/constants.js
@@ -329,8 +329,8 @@ export const RESOURCE_TYPES = {
ITEM: 'item',
INSTANCE: 'instance',
USER: 'user',
- HOLDING: 'holding',
REQUEST_TYPES: 'requestTypes',
+ ECS_TLR_SETTINGS: 'ecsTlrSettings',
};
export const ENTER_EVENT_KEY = 'Enter';
@@ -340,6 +340,11 @@ export const RESOURCE_KEYS = {
barcode: 'barcode',
};
+export const ITEM_QUERIES = {
+ [RESOURCE_KEYS.id]: 'item.id',
+ [RESOURCE_KEYS.barcode]: 'items.barcode',
+};
+
export const REQUEST_FORM_FIELD_NAMES = {
CREATE_TLR: 'createTitleLevelRequest',
FULFILLMENT_PREFERENCE: 'fulfillmentPreference',
@@ -411,3 +416,11 @@ export const DCB_USER = {
export const DCB_INSTANCE_ID = '9d1b77e4-f02e-4b7f-b296-3f2042ddac54';
export const DCB_HOLDINGS_RECORD_ID = '10cd3a5a-d36f-4c7a-bc4f-e1ae3cf820c9';
+
+export const SETTINGS_SCOPES = {
+ CIRCULATION: 'circulation',
+};
+
+export const SETTINGS_KEYS = {
+ GENERAL_TLR: 'generalTlr',
+};
diff --git a/src/routes/RequestQueueRoute.js b/src/routes/RequestQueueRoute.js
index 719358ee..679af841 100644
--- a/src/routes/RequestQueueRoute.js
+++ b/src/routes/RequestQueueRoute.js
@@ -9,7 +9,11 @@ import { stripesConnect } from '@folio/stripes/core';
import RequestQueueView from '../views/RequestQueueView';
import urls from './urls';
-import { requestStatuses } from '../constants';
+import {
+ requestStatuses,
+ SETTINGS_SCOPES,
+ SETTINGS_KEYS,
+} from '../constants';
import {
getTlrSettings,
isPageRequest,
@@ -29,10 +33,10 @@ class RequestQueueRoute extends React.Component {
static manifest = {
configs: {
type: 'okapi',
- records: 'configs',
- path: 'configurations/entries',
+ records: 'items',
+ path: 'settings/entries',
params: {
- query: '(module==SETTINGS and configName==TLR)',
+ query: `(scope==${SETTINGS_SCOPES.CIRCULATION} and key==${SETTINGS_KEYS.GENERAL_TLR})`,
},
},
addressTypes: {
diff --git a/src/routes/RequestsRoute.js b/src/routes/RequestsRoute.js
index d87083db..59e42060 100644
--- a/src/routes/RequestsRoute.js
+++ b/src/routes/RequestsRoute.js
@@ -3,6 +3,7 @@ import {
isEmpty,
isArray,
size,
+ cloneDeep,
} from 'lodash';
import React from 'react';
import PropTypes from 'prop-types';
@@ -23,6 +24,7 @@ import {
IfPermission,
CalloutContext,
TitleManager,
+ checkIfUserInCentralTenant,
} from '@folio/stripes/core';
import {
Button,
@@ -64,7 +66,11 @@ import {
fulfillmentTypeMap,
DEFAULT_REQUEST_TYPE_VALUE,
INPUT_REQUEST_SEARCH_SELECTOR,
+ SETTINGS_SCOPES,
+ SETTINGS_KEYS,
+ ITEM_QUERIES,
PRINT_DETAILS_COLUMNS,
+ RESOURCE_TYPES,
requestFilterTypes,
} from '../constants';
import {
@@ -80,6 +86,7 @@ import {
getSelectedSlipDataMulti,
selectedRowsNonPrintable,
getNextSelectedRowsState,
+ isMultiDataTenant,
} from '../utils';
import packageInfo from '../../package';
import CheckboxColumn from '../components/CheckboxColumn';
@@ -139,24 +146,29 @@ export const getLastPrintedDetails = (printDetails, intl) => {
export const urls = {
user: (value, idType) => {
const query = stringify({ query: `(${idType}=="${value}")` });
+
return `users?${query}`;
},
item: (value, idType) => {
let query;
+ const itemQueryParam = ITEM_QUERIES[idType];
if (isArray(value)) {
- query = `(${value.map((valueItem) => `${idType}=="${valueItem}"`).join(' or ')})`;
+ const queryElements = value.map((valueItem) => `${itemQueryParam}=="${valueItem}"`);
+
+ query = `(${queryElements.join(' or ')})`;
} else {
- query = `(${idType}=="${value}")`;
+ query = `(${itemQueryParam}=="${value}")`;
}
query = stringify({ query });
- return `inventory/items?${query}`;
+
+ return `circulation-bff/requests/search-instances?${query}`;
},
instance: (value) => {
const query = stringify({ query: getInstanceQueryString(value) });
- return `inventory/instances?${query}`;
+ return `circulation-bff/requests/search-instances?${query}`;
},
loan: (value) => {
const query = stringify({ query: `(itemId=="${value}") and status.name==Open` });
@@ -186,11 +198,6 @@ export const urls = {
return `request-preference-storage/request-preference?${query}`;
},
- holding: (value, idType) => {
- const query = stringify({ query: `(${idType}=="${value}")` });
-
- return `holdings-storage/holdings?${query}`;
- },
requestTypes: ({
requesterId,
itemId,
@@ -199,10 +206,10 @@ export const urls = {
operation,
}) => {
if (requestId) {
- return `circulation/requests/allowed-service-points?operation=${operation}&requestId=${requestId}`;
+ return `circulation-bff/requests/allowed-service-points?operation=${operation}&requestId=${requestId}`;
}
- let requestUrl = `circulation/requests/allowed-service-points?requesterId=${requesterId}&operation=${operation}`;
+ let requestUrl = `circulation-bff/requests/allowed-service-points?requesterId=${requesterId}&operation=${operation}`;
if (itemId) {
requestUrl = `${requestUrl}&itemId=${itemId}`;
@@ -212,6 +219,15 @@ export const urls = {
return requestUrl;
},
+ ecsTlrSettings: (value, idType, stripes) => {
+ const isUserInCentralTenant = checkIfUserInCentralTenant(stripes);
+
+ if (isUserInCentralTenant) {
+ return 'tlr/settings';
+ }
+
+ return 'circulation/settings?query=name==ecsTlrFeature';
+ },
};
export const getListFormatter = (
@@ -347,6 +363,12 @@ class RequestsRoute extends React.Component {
staticFallback: { params: {} },
},
},
+ circulationRequests: {
+ type: 'okapi',
+ path: 'circulation-bff/requests',
+ fetch: false,
+ throwErrors: false,
+ },
reportRecords: {
type: 'okapi',
path: 'circulation/requests',
@@ -373,27 +395,6 @@ class RequestsRoute extends React.Component {
limit: MAX_RECORDS,
},
},
- itemUniquenessValidator: {
- type: 'okapi',
- records: 'items',
- accumulate: 'true',
- path: 'inventory/items',
- fetch: false,
- },
- userUniquenessValidator: {
- type: 'okapi',
- records: 'users',
- accumulate: 'true',
- path: 'users',
- fetch: false,
- },
- instanceUniquenessValidator: {
- type: 'okapi',
- records: 'instances',
- accumulate: true,
- path: 'inventory/instances',
- fetch: false,
- },
patronBlocks: {
type: 'okapi',
records: 'manualblocks',
@@ -474,10 +475,10 @@ class RequestsRoute extends React.Component {
},
configs: {
type: 'okapi',
- records: 'configs',
- path: 'configurations/entries',
+ records: 'items',
+ path: 'settings/entries',
params: {
- query: '(module==SETTINGS and configName==TLR)',
+ query: `(scope==${SETTINGS_SCOPES.CIRCULATION} and key==${SETTINGS_KEYS.GENERAL_TLR})`,
},
},
circulationSettings: {
@@ -507,6 +508,9 @@ class RequestsRoute extends React.Component {
GET: PropTypes.func,
POST: PropTypes.func,
}),
+ circulationRequests: PropTypes.shape({
+ POST: PropTypes.func,
+ }),
reportRecords: PropTypes.shape({
GET: PropTypes.func,
}),
@@ -643,6 +647,8 @@ class RequestsRoute extends React.Component {
this.expiredHoldsReportColumnHeaders = this.getColumnHeaders(expiredHoldsReportHeaders);
this.state = {
+ isEcsTlrSettingReceived: false,
+ isEcsTlrSettingEnabled: false,
csvReportPending: false,
submitting: false,
errorMessage: '',
@@ -680,13 +686,23 @@ class RequestsRoute extends React.Component {
}
componentDidMount() {
+ const { stripes } = this.props;
+
this.setCurrentServicePointId();
+
+ if (stripes?.user?.user?.tenants) {
+ this.getEcsTlrSettings();
+ }
}
componentDidUpdate(prevProps) {
+ const { stripes } = this.props;
+ const {
+ submitting,
+ isViewPrintDetailsEnabled,
+ } = this.state;
const patronBlocks = get(this.props.resources, ['patronBlocks', 'records'], []);
const prevBlocks = get(prevProps.resources, ['patronBlocks', 'records'], []);
- const { submitting, isViewPrintDetailsEnabled } = this.state;
const prevExpired = prevBlocks.filter(p => moment(moment(p.expirationDate).format()).isSameOrBefore(moment().format()) && p.expirationDate) || [];
const expired = patronBlocks.filter(p => moment(moment(p.expirationDate).format()).isSameOrBefore(moment().format()) && p.expirationDate) || [];
const { id: currentServicePointId } = this.getCurrentServicePointInfo();
@@ -738,6 +754,41 @@ class RequestsRoute extends React.Component {
if (!isViewPrintDetailsEnabled) {
this.handlePrintDetailsDisabled();
}
+
+ if (stripes?.user?.user?.tenants && stripes.user.user !== prevProps.stripes?.user?.user) {
+ this.getEcsTlrSettings();
+ }
+ }
+
+ /* For multi data tenant environments
+ * ECS TLR setting has to be retrieved (Settings>Circulation>Consortium title level requests (TLR)).
+ * In a case if this setting is enabled we should hide Move and Duplicate buttons in action menu. */
+ getEcsTlrSettings = () => {
+ const { stripes } = this.props;
+
+ if (isMultiDataTenant(stripes)) {
+ this.findResource(RESOURCE_TYPES.ECS_TLR_SETTINGS)
+ .then(res => {
+ let isEcsTlrSettingEnabled;
+
+ if (checkIfUserInCentralTenant(stripes)) {
+ isEcsTlrSettingEnabled = res?.ecsTlrFeatureEnabled;
+ } else {
+ isEcsTlrSettingEnabled = res?.circulationSettings?.[0]?.value?.enabled;
+ }
+
+ this.setState({
+ isEcsTlrSettingReceived: true,
+ isEcsTlrSettingEnabled,
+ });
+ })
+ .catch(() => {
+ this.setState({
+ isEcsTlrSettingReceived: false,
+ isEcsTlrSettingEnabled: false,
+ });
+ });
+ }
}
handlePrintDetailsDisabled() {
@@ -949,9 +1000,12 @@ class RequestsRoute extends React.Component {
// idType can be 'id', 'barcode', etc.
findResource(resource, value, idType = 'id') {
- const query = urls[resource](value, idType);
+ const { stripes } = this.props;
+ const query = urls[resource](value, idType, stripes);
- return fetch(`${this.okapiUrl}/${query}`, this.httpHeadersOptions).then(response => response.json());
+ return fetch(`${this.okapiUrl}/${query}`, this.httpHeadersOptions)
+ .then(response => response.json())
+ .catch(() => null);
}
toggleModal() {
@@ -1048,26 +1102,34 @@ class RequestsRoute extends React.Component {
this.props.mutator.activeRecord.update({ patronId: patron.id });
};
- create = (data) => {
+ create = (requestData) => {
+ const userPersonalData = cloneDeep(requestData?.requester?.personal);
const query = new URLSearchParams(this.props.location.search);
const mode = query.get('mode');
- return this.props.mutator.records.POST(data)
- .then(() => {
- this.closeLayer();
+ return this.props.mutator.circulationRequests.POST(requestData)
+ .then((res) => {
+ const {
+ match: {
+ path,
+ },
+ history,
+ } = this.props;
+
+ history.push(`${path}/view/${res?.primaryRequestId || res?.id}`);
this.context.sendCallout({
message: isDuplicateMode(mode)
? (
)
: (
),
});
@@ -1381,6 +1443,8 @@ class RequestsRoute extends React.Component {
holdsShelfReportPending,
createTitleLevelRequestsByDefault,
isViewPrintDetailsEnabled,
+ isEcsTlrSettingReceived,
+ isEcsTlrSettingEnabled,
} = this.state;
const isPrintHoldRequestsEnabled = getPrintHoldRequestsEnabled(resources.printHoldRequests);
const { name: servicePointName } = this.getCurrentServicePointInfo();
@@ -1678,6 +1742,8 @@ class RequestsRoute extends React.Component {
query: resources.query,
onDuplicate: this.onDuplicate,
buildRecordsForHoldsShelfReport: this.buildRecordsForHoldsShelfReport,
+ isEcsTlrSettingReceived,
+ isEcsTlrSettingEnabled,
}}
viewRecordOnCollapse={this.viewRecordOnCollapse}
viewRecordPerms="ui-requests.view"
diff --git a/src/routes/RequestsRoute.test.js b/src/routes/RequestsRoute.test.js
index 182c4298..7ef69477 100644
--- a/src/routes/RequestsRoute.test.js
+++ b/src/routes/RequestsRoute.test.js
@@ -20,6 +20,7 @@ import {
CalloutContext,
AppIcon,
TitleManager,
+ checkIfUserInCentralTenant,
} from '@folio/stripes/core';
import {
TextLink,
@@ -28,6 +29,7 @@ import {
import {
exportCsv,
effectiveCallNumber,
+ getHeaderWithCredentials,
} from '@folio/stripes/util';
import RequestsRoute, {
@@ -46,6 +48,7 @@ import {
getFullName,
getInstanceQueryString,
getNextSelectedRowsState,
+ isMultiDataTenant,
} from '../utils';
import {
getFormattedYears,
@@ -64,6 +67,7 @@ import {
MAX_RECORDS,
REQUEST_OPERATIONS,
INPUT_REQUEST_SEARCH_SELECTOR,
+ ITEM_QUERIES,
PRINT_DETAILS_COLUMNS,
} from '../constants';
import { historyData } from '../../test/jest/fixtures/historyData';
@@ -84,7 +88,6 @@ const testIds = {
rowCheckbox: 'rowCheckbox',
selectRequestCheckbox: 'selectRequestCheckbox',
};
-
const intlCache = createIntlCache();
const intl = createIntl(
{
@@ -110,6 +113,8 @@ jest.mock('../utils', () => ({
getInstanceQueryString: jest.fn(),
getNextSelectedRowsState: jest.fn(),
extractPickSlipRequestIds: jest.fn(),
+ isMultiDataTenant: jest.fn(),
+ generateUserName: jest.fn(),
}));
jest.mock('./utils', () => ({
...jest.requireActual('./utils'),
@@ -235,6 +240,12 @@ const mockedRequest = {
},
id: 'requestId',
};
+const userData = {
+ requester: {
+ personal: {},
+ }
+};
+const createRequestButtonLabel = 'Create request';
const printDetailsMockData = {
printCount: 11,
printEventDate: '2024-08-03T13:33:31.868Z',
@@ -261,6 +272,7 @@ SearchAndSort.mockImplementation(jest.fn(({
customPaneSub,
columnMapping,
resultsFormatter,
+ onCreate,
}) => {
const onClickActions = () => {
onDuplicate(parentResources.records.records[0]);
@@ -312,6 +324,12 @@ SearchAndSort.mockImplementation(jest.fn(({
})}
>onFilterChange
+
{actionMenu({ onToggle: jest.fn() })}
@@ -423,6 +441,9 @@ describe('RequestsRoute', () => {
GET: jest.fn(),
POST: jest.fn().mockResolvedValue(),
},
+ circulationRequests: {
+ POST: jest.fn().mockResolvedValue(),
+ },
reportRecords: {
GET: jest.fn().mockReturnValueOnce(mockRecordValues).mockRejectedValue(),
reset: jest.fn(),
@@ -675,6 +696,138 @@ describe('RequestsRoute', () => {
});
});
+ describe('When single data tenant', () => {
+ const response = {
+ id: 'responseId',
+ };
+ const props = {
+ ...defaultProps,
+ mutator: {
+ ...defaultProps.mutator,
+ circulationRequests: {
+ POST: jest.fn().mockResolvedValue(response),
+ },
+ },
+ };
+
+ beforeEach(() => {
+ isMultiDataTenant.mockReturnValue(false);
+ renderComponent(props);
+ });
+
+ it('should handle request creation', () => {
+ const createRequestButton = screen.getByText(createRequestButtonLabel);
+
+ fireEvent.click(createRequestButton);
+
+ expect(props.mutator.circulationRequests.POST).toHaveBeenCalledWith(userData);
+ });
+
+ it('should redirect to details page', async () => {
+ const createRequestButton = screen.getByText(createRequestButtonLabel);
+
+ fireEvent.click(createRequestButton);
+
+ await waitFor(() => {
+ expect(props.history.push).toHaveBeenCalledWith(`${props.match.path}/view/${response.id}`);
+ });
+ });
+
+ it('should send callout', async () => {
+ const createRequestButton = screen.getByText(createRequestButtonLabel);
+
+ sendCallout.mockClear();
+ fireEvent.click(createRequestButton);
+
+ await waitFor(() => {
+ expect(sendCallout).toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('When multi data tenant', () => {
+ const requestHeaders = { test: 'test' };
+ const fetchSpy = jest.fn();
+ const response = {
+ id: 'responseId',
+ };
+ const props = {
+ ...defaultProps,
+ mutator: {
+ ...defaultProps.mutator,
+ circulationRequests: {
+ POST: jest.fn().mockResolvedValue(response),
+ },
+ },
+ stripes: {
+ ...defaultProps.stripes,
+ user: {
+ user: {
+ ...defaultProps.stripes.user.user,
+ tenants: [{ id: 'tenantId' }],
+ },
+ },
+ },
+ };
+
+ beforeEach(() => {
+ isMultiDataTenant.mockReturnValue(true);
+ getHeaderWithCredentials.mockReturnValue(requestHeaders);
+ });
+
+ afterEach(() => {
+ fetchSpy.mockClear();
+ });
+
+ describe('When user in central tenant', () => {
+ beforeEach(() => {
+ fetchSpy.mockResolvedValueOnce({
+ json: () => ({
+ ecsTlrFeatureEnabled: true,
+ }),
+ });
+ checkIfUserInCentralTenant.mockReturnValue(true);
+ global.fetch = fetchSpy;
+ renderComponent(props);
+ });
+
+ it('should use correct endpoint to get ecs tlr settings', () => {
+ expect(fetchSpy).toHaveBeenCalledWith(`${defaultProps.stripes.okapi.url}/tlr/settings`, requestHeaders);
+ });
+
+ it('should handle request creation', () => {
+ const createRequestButton = screen.getByText(createRequestButtonLabel);
+
+ fireEvent.click(createRequestButton);
+
+ expect(props.mutator.circulationRequests.POST).toHaveBeenCalledWith(userData);
+ });
+ });
+
+ describe('When user in data tenant', () => {
+ beforeEach(() => {
+ fetchSpy.mockResolvedValueOnce({
+ json: () => ({
+ circulationSettings: [
+ {
+ value: {
+ enabled: true,
+ },
+ }
+ ],
+ }),
+ });
+ checkIfUserInCentralTenant.mockReturnValueOnce(false);
+ global.fetch = fetchSpy;
+ renderComponent(props);
+ });
+
+ it('should use correct endpoint to get ecs tlr settings', () => {
+ expect(fetchSpy).toHaveBeenCalledWith(`${defaultProps.stripes.okapi.url}/circulation/settings?query=name==ecsTlrFeature`, requestHeaders);
+ });
+ });
+ });
+
describe('Print pick slips', () => {
afterEach(() => {
jest.clearAllMocks();
@@ -1461,7 +1614,7 @@ describe('RequestsRoute', () => {
describe('urls', () => {
const mockedQueryValue = 'testQuery';
- const idType = 'idType';
+ const idType = 'id';
beforeEach(() => {
stringify.mockReturnValue(mockedQueryValue);
@@ -1501,14 +1654,14 @@ describe('RequestsRoute', () => {
it('should trigger "stringify" with correct argument', () => {
const expectedArgument = {
- query: `(${idType}=="${value[0]}" or ${idType}=="${value[1]}")`,
+ query: `(${ITEM_QUERIES[idType]}=="${value[0]}" or ${ITEM_QUERIES[idType]}=="${value[1]}")`,
};
expect(stringify).toHaveBeenCalledWith(expectedArgument);
});
it('should return correct url', () => {
- const expectedResult = `inventory/items?${mockedQueryValue}`;
+ const expectedResult = `circulation-bff/requests/search-instances?${mockedQueryValue}`;
expect(queryString).toBe(expectedResult);
});
@@ -1524,14 +1677,14 @@ describe('RequestsRoute', () => {
it('should trigger "stringify" with correct argument', () => {
const expectedArgument = {
- query: `(${idType}=="${value}")`,
+ query: `(${ITEM_QUERIES[idType]}=="${value}")`,
};
expect(stringify).toHaveBeenCalledWith(expectedArgument);
});
it('should return correct url', () => {
- const expectedResult = `inventory/items?${mockedQueryValue}`;
+ const expectedResult = `circulation-bff/requests/search-instances?${mockedQueryValue}`;
expect(queryString).toBe(expectedResult);
});
@@ -1561,7 +1714,7 @@ describe('RequestsRoute', () => {
});
it('should return correct url', () => {
- const expectedResult = `inventory/instances?${mockedQueryValue}`;
+ const expectedResult = `circulation-bff/requests/search-instances?${mockedQueryValue}`;
expect(queryString).toBe(expectedResult);
});
@@ -1673,63 +1826,65 @@ describe('RequestsRoute', () => {
});
});
- describe('holding', () => {
- const value = 'value';
- let queryString;
-
- beforeEach(() => {
- queryString = urls.holding(value, idType);
- });
-
- it('should trigger "stringify" with correct argument', () => {
- const expectedArgument = {
- query: `(${idType}=="${value}")`,
- };
-
- expect(stringify).toHaveBeenCalledWith(expectedArgument);
- });
-
- it('should return correct url', () => {
- const expectedResult = `holdings-storage/holdings?${mockedQueryValue}`;
-
- expect(queryString).toBe(expectedResult);
- });
- });
-
describe('requestTypes', () => {
const requesterId = 'requesterIdUrl';
const operation = REQUEST_OPERATIONS.CREATE;
const itemId = 'itemIdUrl';
const instanceId = 'instanceIdUrl';
+ const requestUrl = 'circulation-bff/requests/allowed-service-points';
it('should return url with "itemId"', () => {
- const expectedUrl = `circulation/requests/allowed-service-points?requesterId=${requesterId}&operation=${operation}&itemId=${itemId}`;
+ const expectedResult = `${requestUrl}?requesterId=${requesterId}&operation=${operation}&itemId=${itemId}`;
expect(urls.requestTypes({
requesterId,
itemId,
operation,
- })).toBe(expectedUrl);
+ })).toBe(expectedResult);
});
it('should return url with "instanceId"', () => {
- const expectedUrl = `circulation/requests/allowed-service-points?requesterId=${requesterId}&operation=${operation}&instanceId=${instanceId}`;
+ const expectedResult = `${requestUrl}?requesterId=${requesterId}&operation=${operation}&instanceId=${instanceId}`;
expect(urls.requestTypes({
requesterId,
instanceId,
operation,
- })).toBe(expectedUrl);
+ })).toBe(expectedResult);
});
it('should return url with "requestId"', () => {
const requestId = 'requestId';
- const expectedUrl = `circulation/requests/allowed-service-points?operation=${operation}&requestId=${requestId}`;
+ const expectedResult = `${requestUrl}?operation=${operation}&requestId=${requestId}`;
expect(urls.requestTypes({
requestId,
operation,
- })).toBe(expectedUrl);
+ })).toBe(expectedResult);
+ });
+ });
+
+ describe('ecsTlrSettings', () => {
+ describe('When user in central tenant', () => {
+ it('should return correct endpoint', () => {
+ checkIfUserInCentralTenant.mockReturnValueOnce(true);
+
+ const ecsTlrSettingsEndpoint = 'tlr/settings';
+ const result = urls.ecsTlrSettings();
+
+ expect(result).toBe(ecsTlrSettingsEndpoint);
+ });
+ });
+
+ describe('When user in data tenant', () => {
+ it('should return correct endpoint', () => {
+ checkIfUserInCentralTenant.mockReturnValueOnce(false);
+
+ const ecsTlrSettingsEndpoint = 'circulation/settings?query=name==ecsTlrFeature';
+ const result = urls.ecsTlrSettings();
+
+ expect(result).toBe(ecsTlrSettingsEndpoint);
+ });
});
});
});
diff --git a/src/utils.js b/src/utils.js
index 2b4e961c..cdaf3962 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -354,13 +354,7 @@ export function parseErrorMessage(errorMessage) {
));
}
-export const getTlrSettings = (settings) => {
- try {
- return JSON.parse(settings);
- } catch (error) {
- return {};
- }
-};
+export const getTlrSettings = (settings) => settings || {};
export const getRequestLevelValue = (value) => {
return value
@@ -371,15 +365,19 @@ export const getRequestLevelValue = (value) => {
export const getInstanceQueryString = (hrid, id) => `("hrid"=="${hrid}" or "id"=="${id || hrid}")`;
export const generateUserName = (user) => {
- const {
- firstName,
- lastName,
- middleName,
- } = user;
+ if (user) {
+ const {
+ firstName,
+ lastName,
+ middleName,
+ } = user;
- const shownMiddleName = middleName ? ` ${middleName}` : '';
+ const shownMiddleName = middleName ? ` ${middleName}` : '';
- return `${lastName}${firstName ? ', ' + firstName + shownMiddleName : ''}`;
+ return `${lastName}${firstName ? ', ' + firstName + shownMiddleName : ''}`;
+ }
+
+ return '';
};
export const handleKeyCommand = (handler, { disabled } = {}) => {
@@ -519,6 +517,10 @@ export function resetFieldState(form, fieldName) {
}
}
+export const isMultiDataTenant = (stripes) => {
+ return stripes.hasInterface('consortia') && stripes.hasInterface('ecs-tlr');
+};
+
export const getRequester = (proxy, selectedUser) => {
if (proxy && proxy.id !== selectedUser?.id) {
return proxy;
diff --git a/src/utils.test.js b/src/utils.test.js
index f5ff4bb5..c46e65fb 100644
--- a/src/utils.test.js
+++ b/src/utils.test.js
@@ -34,6 +34,7 @@ import {
selectedRowsNonPrintable,
isPrintable,
getNextSelectedRowsState,
+ isMultiDataTenant,
getRequester,
getFullName,
} from './utils';
@@ -193,17 +194,13 @@ describe('getTlrSettings', () => {
createTitleLevelRequestsByDefault: false,
};
- it('should return parsed settings', () => {
- expect(getTlrSettings(JSON.stringify(defaultSettings))).toEqual(defaultSettings);
+ it('should return passed settings', () => {
+ expect(getTlrSettings(defaultSettings)).toEqual(defaultSettings);
});
it('should return empty object if nothing passed', () => {
expect(getTlrSettings()).toEqual({});
});
-
- it('should return empty object if invalid settings passed', () => {
- expect(getTlrSettings("{'foo': 1}")).toEqual({});
- });
});
describe('getRequestLevelValue', () => {
@@ -275,6 +272,10 @@ describe('generateUserName', () => {
expect(generateUserName({ firstName, lastName, middleName }))
.toEqual(lastName);
});
+
+ it('should return empty string', () => {
+ expect(generateUserName()).toBe('');
+ });
});
describe('handlekeycommand', () => {
@@ -1097,6 +1098,32 @@ describe('getRequestTypeOptions', () => {
});
});
+describe('isMultiDataTenant', () => {
+ describe('When multi data tenant', () => {
+ const stripes = {
+ hasInterface: () => true,
+ };
+
+ it('should return true', () => {
+ const result = isMultiDataTenant(stripes);
+
+ expect(result).toBe(true);
+ });
+ });
+
+ describe('When single data tenant', () => {
+ const stripes = {
+ hasInterface: () => false,
+ };
+
+ it('should return false', () => {
+ const result = isMultiDataTenant(stripes);
+
+ expect(result).toBe(false);
+ });
+ });
+});
+
describe('getRequester', () => {
const selectedUser = {
id: 'selectedUserId',
diff --git a/test/jest/__mock__/stripesCore.mock.js b/test/jest/__mock__/stripesCore.mock.js
index a7f2092e..bc937c7b 100644
--- a/test/jest/__mock__/stripesCore.mock.js
+++ b/test/jest/__mock__/stripesCore.mock.js
@@ -6,6 +6,7 @@ const mockStripes = buildStripes();
jest.mock('@folio/stripes/core', () => ({
...jest.requireActual('@folio/stripes/core'),
AppContextMenu: ({ children }) => (typeof children === 'function' ? children(jest.fn()) : children),
+ checkIfUserInCentralTenant: jest.fn(),
IntlConsumer: jest.fn(({ children }) => {
const intl = {
formatMessage: jest.fn(({ id }) => id),