diff --git a/CHANGELOG.md b/CHANGELOG.md index ada3de9f2..1de2d36da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,11 @@ # Change history for ui-inventory +## [11.0.0] IN PROGRESS -## [10.1.0] IN PROGRESS - +* *BREAKING* Replace imports from quick-marc with stripes-marc-components. Refs UIIN-2636. * Make Inventory search and browse query boxes expandable. Refs UIIN-2493. +* Added support for `containsAny` match option in Advanced search. Refs UIIN-2486. +* Inventory search/browse: Do not retain checkbox selections when toggling search segment. Refs UIIN-2477. ## [10.0.1] IN PROGRESS diff --git a/package.json b/package.json index 4dc9374e4..1690b7162 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@folio/inventory", - "version": "10.1.0", + "version": "11.0.0", "description": "Inventory manager", "repository": "folio-org/ui-inventory", "publishConfig": { @@ -862,6 +862,7 @@ "@folio/stripes-components": "^12.0.0", "@folio/stripes-connect": "^9.0.0", "@folio/stripes-core": "^10.0.0", + "@folio/stripes-marc-components": "^1.0.0", "@folio/stripes-smart-components": "^9.0.0", "@folio/stripes-testing": "^4.6.0", "@folio/stripes-util": "^6.0.0", @@ -882,7 +883,6 @@ "zustand": "^4.1.1" }, "dependencies": { - "@folio/quick-marc": "^7.0.0", "@folio/stripes-acq-components": "^5.0.0", "classnames": "^2.3.2", "file-saver": "^2.0.0", @@ -904,6 +904,7 @@ }, "peerDependencies": { "@folio/stripes": "^9.0.0", + "@folio/stripes-marc-components": "^1.0.0", "react": "^18.2.0", "react-intl": "^6.4.4", "react-query": "^3.6.0", diff --git a/src/ViewHoldingsRecord.js b/src/ViewHoldingsRecord.js index 81d06a913..218794b9e 100644 --- a/src/ViewHoldingsRecord.js +++ b/src/ViewHoldingsRecord.js @@ -81,7 +81,7 @@ class ViewHoldingsRecord extends React.Component { path: 'holdings-storage/holdings/:{holdingsrecordid}', resourceShouldRefresh: false, accumulate: true, - tenant: '!{location.state.tenantTo}' + tenant: '!{tenantTo}', }, items: { type: 'okapi', @@ -97,7 +97,7 @@ class ViewHoldingsRecord extends React.Component { type: 'okapi', path: 'inventory/instances/:{id}', accumulate: true, - tenant: '!{location.state.tenantTo}' + tenant: '!{tenantTo}', }, tagSettings: { type: 'okapi', diff --git a/src/components/InstancesList/InstancesList.js b/src/components/InstancesList/InstancesList.js index 2626f3d83..83dce4f6b 100644 --- a/src/components/InstancesList/InstancesList.js +++ b/src/components/InstancesList/InstancesList.js @@ -393,6 +393,11 @@ class InstancesList extends React.Component { document.getElementById('input-inventory-search').focus(); } + handleSearchSegmentChange = (segment) => { + this.refocusOnInputSearch(segment); + this.setState({ selectedRows: {} }); + } + onSearchModeSwitch = () => { const { namespace, @@ -415,7 +420,7 @@ class InstancesList extends React.Component { /> ); diff --git a/src/components/InstancesList/InstancesList.test.js b/src/components/InstancesList/InstancesList.test.js index 2680d5c2f..cb1324a32 100644 --- a/src/components/InstancesList/InstancesList.test.js +++ b/src/components/InstancesList/InstancesList.test.js @@ -221,6 +221,22 @@ describe('InstancesList', () => { }); }); + describe('when search segment is changed', () => { + it('should clear selected rows', () => { + const { + getAllByLabelText, + getByText, + } = renderInstancesList({ + segment: 'instances', + }); + + fireEvent.click(getAllByLabelText('Select instance')[0]); + fireEvent.click(getByText('Holdings')); + + expect(getAllByLabelText('Select instance')[0].checked).toBeFalsy(); + }); + }); + describe('when a user performs a search and clicks the `Next` button in the list of records', () => { describe('then clicks on the `Browse` lookup tab and then clicks `Search` lookup tab', () => { it('should avoid infinity loading by resetting the records on unmounting', () => { diff --git a/src/components/ViewSource/ViewSource.js b/src/components/ViewSource/ViewSource.js index ce8fa5cae..57fab5307 100644 --- a/src/components/ViewSource/ViewSource.js +++ b/src/components/ViewSource/ViewSource.js @@ -10,9 +10,11 @@ import { LoadingView, } from '@folio/stripes/components'; import { useStripes } from '@folio/stripes/core'; -import MarcView from '@folio/quick-marc/src/QuickMarcView/QuickMarcView'; -import PrintPopup from '@folio/quick-marc/src/QuickMarcView/PrintPopup'; -import { getHeaders } from '@folio/quick-marc/src/QuickMarcEditor/utils'; +import { + MarcView, + PrintPopup, + getHeaders, +} from '@folio/stripes-marc-components'; import { useGoBack } from '../../common/hooks'; diff --git a/src/components/ViewSource/ViewSource.test.js b/src/components/ViewSource/ViewSource.test.js index a3777ab79..a719b9381 100644 --- a/src/components/ViewSource/ViewSource.test.js +++ b/src/components/ViewSource/ViewSource.test.js @@ -89,18 +89,18 @@ describe('ViewSource', () => { }); }); - it('should render QuickMarcView', () => { - expect(screen.getByText('QuickMarcView')).toBeInTheDocument(); + it('should render MarcView', () => { + expect(screen.getByText('MarcView')).toBeInTheDocument(); }); it('should initiate useGoBack with correct path', () => { expect(useGoBack).toBeCalledWith('/inventory/view/instance-id'); }); - describe('when QuickMarcView is closed', () => { + describe('when MarcView is closed', () => { it('should call onClose with correct url', async () => { - await waitFor(() => expect(screen.getByText('QuickMarcView')).toBeInTheDocument()); - act(() => fireEvent.click(screen.getByText('QuickMarcView'))); + await waitFor(() => expect(screen.getByText('MarcView')).toBeInTheDocument()); + act(() => fireEvent.click(screen.getByText('MarcView'))); expect(mockGoBack).toBeCalledTimes(1); }); }); diff --git a/src/constants.js b/src/constants.js index 534d4d280..2aaa0852c 100644 --- a/src/constants.js +++ b/src/constants.js @@ -466,146 +466,175 @@ export const fieldSearchConfigurations = { exactPhrase: 'keyword=="%{query.query}"', containsAll: 'keyword all "%{query.query}"', startsWith: 'keyword all "%{query.query}*"', + containsAny: 'keyword any "%{query.query}"', }, contributor: { exactPhrase: 'contributors.name=="%{query.query}"', containsAll: 'contributors.name="*%{query.query}*"', startsWith: 'contributors.name="%{query.query}*"', + containsAny: 'contributors.name any "*%{query.query}*"', }, title: { exactPhrase: 'title=="%{query.query}"', containsAll: 'title all "%{query.query}"', startsWith: 'title all "%{query.query}*"', + containsAny: 'title any "%{query.query}"', }, isbn: { exactPhrase: 'isbn=="%{query.query}"', containsAll: 'isbn="*%{query.query}*"', startsWith: 'isbn="%{query.query}*"', + containsAny: 'isbn any "*%{query.query}*"', }, issn: { exactPhrase: 'issn=="%{query.query}"', containsAll: 'issn="*%{query.query}*"', startsWith: 'issn="%{query.query}*"', + containsAny: 'issn any "*%{query.query}*"', }, identifier: { exactPhrase: 'identifiers.value=="%{query.query}"', containsAll: 'identifiers.value="*%{query.query}*"', startsWith: 'identifiers.value="%{query.query}*"', + containsAny: 'identifiers.value any "*%{query.query}*"', }, oclc: { exactPhrase: 'oclc=="%{query.query}"', containsAll: 'oclc="*%{query.query}*"', startsWith: 'oclc="%{query.query}*"', + containsAny: 'oclc any "*%{query.query}*"', }, instanceNotes: { exactPhrase: 'notes.note=="%{query.query}" or administrativeNotes=="%{query.query}"', containsAll: 'notes.note all "%{query.query}" or administrativeNotes all "%{query.query}"', startsWith: 'notes.note all "%{query.query}*" or administrativeNotes all "%{query.query}*"', + containsAny: 'notes.note any "%{query.query}" or administrativeNotes any "%{query.query}"', }, instanceAdministrativeNotes: { exactPhrase: 'administrativeNotes=="%{query.query}"', containsAll: 'administrativeNotes all "%{query.query}"', startsWith: 'administrativeNotes all "%{query.query}*"', + containsAny: 'administrativeNotes any "%{query.query}"', }, subject: { exactPhrase: 'subjects.value=="%{query.query}"', containsAll: 'subjects.value all "%{query.query}"', startsWith: 'subjects.value=="%{query.query}*"', + containsAny: 'subjects.value any "%{query.query}"', }, callNumber: { exactPhrase: 'itemEffectiveShelvingOrder=="%{query.query}"', containsAll: 'itemEffectiveShelvingOrder="*%{query.query}*"', startsWith: 'itemEffectiveShelvingOrder=="%{query.query}*"', + containsAny: 'itemEffectiveShelvingOrder any "*%{query.query}*"', }, hrid: { exactPhrase: 'hrid=="%{query.query}"', containsAll: 'hrid=="*%{query.query}*"', startsWith: 'hrid=="%{query.query}*"', + containsAny: 'hrid any "*%{query.query}*"', }, id: { exactPhrase: 'id=="%{query.query}"', containsAll: 'id="*%{query.query}*"', startsWith: 'id="%{query.query}*"', + containsAny: 'id any "*%{query.query}*"', }, authorityId: { exactPhrase: 'authorityId == %{query.query}', containsAll: 'authorityId=="*%{query.query}*"', startsWith: 'authorityId=="%{query.query}*"', + containsAny: 'authorityId any "*%{query.query}*"', }, allFields: { exactPhrase: 'cql.all=="%{query.query}"', containsAll: 'cql.all all "%{query.query}"', startsWith: 'cql.all all "%{query.query}*"', + containsAny: 'cql.all any "%{query.query}"', }, holdingsFullCallNumbers: { exactPhrase: 'holdingsFullCallNumbers=="%{query.query}"', containsAll: 'holdingsFullCallNumbers="*%{query.query}*"', startsWith: 'holdingsFullCallNumbers="%{query.query}*"', + containsAny: 'holdingsFullCallNumbers any "*%{query.query}*"', }, holdingsNormalizedCallNumbers: { exactPhrase: 'holdingsNormalizedCallNumbers=="%{query.query}"', containsAll: 'holdingsNormalizedCallNumbers="*%{query.query}*"', startsWith: 'holdingsNormalizedCallNumbers="%{query.query}*"', + containsAny: 'holdingsNormalizedCallNumbers any "*%{query.query}*"', }, holdingsNotes: { exactPhrase: 'holdings.notes.note=="%{query.query}" or holdings.administrativeNotes=="%{query.query}"', containsAll: 'holdings.notes.note all "%{query.query}" or holdings.administrativeNotes all "%{query.query}"', startsWith: 'holdings.notes.note all "%{query.query}*" or holdings.administrativeNotes all "%{query.query}*"', + containsAny: 'holdings.notes.note any "%{query.query}" or holdings.administrativeNotes any "%{query.query}"', }, holdingsAdministrativeNotes: { exactPhrase: 'holdings.administrativeNotes=="%{query.query}"', containsAll: 'holdings.administrativeNotes all "%{query.query}"', startsWith: 'holdings.administrativeNotes all "%{query.query}*"', + containsAny: 'holdings.administrativeNotes any "%{query.query}"', }, holdingsHrid: { exactPhrase: 'holdings.hrid=="%{query.query}"', containsAll: 'holdings.hrid=="*%{query.query}*"', startsWith: 'holdings.hrid=="%{query.query}*"', + containsAny: 'holdings.hrid any "*%{query.query}*"', }, hid: { exactPhrase: 'holdings.id=="%{query.query}"', containsAll: 'holdings.id="*%{query.query}*"', startsWith: 'holdings.id="%{query.query}*"', + containsAny: 'holdings.id any "*%{query.query}*"', }, barcode: { exactPhrase: 'items.barcode=="%{query.query}"', containsAll: 'items.barcode="*%{query.query}*"', startsWith: 'items.barcode="%{query.query}*"', + containsAny: 'items.barcode any "*%{query.query}*"', }, itemFullCallNumbers: { exactPhrase: 'itemFullCallNumbers=="%{query.query}"', containsAll: 'itemFullCallNumbers="*%{query.query}*"', startsWith: 'itemFullCallNumbers="%{query.query}*"', + containsAny: 'itemFullCallNumbers any "*%{query.query}*"', }, itemNormalizedCallNumbers: { exactPhrase: 'itemNormalizedCallNumbers=="%{query.query}"', containsAll: 'itemNormalizedCallNumbers="*%{query.query}*"', startsWith: 'itemNormalizedCallNumbers="%{query.query}*"', + containsAny: 'itemNormalizedCallNumbers any "*%{query.query}*"', }, itemNotes: { exactPhrase: 'item.notes.note=="%{query.query}" or item.administrativeNotes=="%{query.query}"', containsAll: 'item.notes.note all "%{query.query}" or item.administrativeNotes all "%{query.query}"', startsWith: 'item.notes.note all "%{query.query}*" or item.administrativeNotes all "%{query.query}*"', + containsAny: 'item.notes.note any "%{query.query}" or item.administrativeNotes any "%{query.query}"', }, itemAdministrativeNotes: { exactPhrase: 'item.administrativeNotes=="%{query.query}"', containsAll: 'item.administrativeNotes all "%{query.query}"', startsWith: 'item.administrativeNotes all "%{query.query}*"', + containsAny: 'item.administrativeNotes any "%{query.query}"', }, itemCirculationNotes: { exactPhrase: 'item.circulationNotes.note=="%{query.query}"', containsAll: 'item.circulationNotes.note all "%{query.query}"', startsWith: 'item.circulationNotes.note all "%{query.query}*"', + containsAny: 'item.circulationNotes.note any "%{query.query}"', }, itemHrid: { exactPhrase: 'items.hrid=="%{query.query}"', containsAll: 'items.hrid="*%{query.query}*"', startsWith: 'items.hrid="%{query.query}*"', + containsAny: 'items.hrid any "*%{query.query}*"', }, iid: { exactPhrase: 'item.id=="%{query.query}"', containsAll: 'item.id="*%{query.query}*"', startsWith: 'item.id="%{query.query}*"', + containsAny: 'item.id any "*%{query.query}*"', }, }; diff --git a/src/routes/ItemRoute.js b/src/routes/ItemRoute.js index 729e04133..d9a14fd2e 100644 --- a/src/routes/ItemRoute.js +++ b/src/routes/ItemRoute.js @@ -1,190 +1,26 @@ import React from 'react'; import PropTypes from 'prop-types'; -import { - flowRight, - get, -} from 'lodash'; +import { flowRight } from 'lodash'; import { stripesConnect } from '@folio/stripes/core'; -import { requestsStatusString } from '../Instance/ViewRequests/utils'; - import withLocation from '../withLocation'; import { ItemView } from '../views'; -import { PaneLoading } from '../components'; import { DataContext } from '../contexts'; -const getRequestsPath = `circulation/requests?query=(itemId==:{itemid}) and status==(${requestsStatusString}) sortby requestDate desc&limit=1`; - class ItemRoute extends React.Component { - static manifest = Object.freeze({ - query: {}, - itemsResource: { - type: 'okapi', - path: 'inventory/items/:{itemid}', - POST: { path: 'inventory/items' }, - resourceShouldRefresh: true, - tenant: '!{location.state.tenantTo}', - }, - markItemAsWithdrawn: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-withdrawn', - }, - clientGeneratePk: false, - fetch: false, - }, - markItemAsMissing: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-missing', - }, - clientGeneratePk: false, - fetch: false, - }, - markAsInProcess: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-in-process', - }, - clientGeneratePk: false, - fetch: false, - }, - markAsInProcessNonRequestable: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-in-process-non-requestable', - }, - clientGeneratePk: false, - fetch: false, - }, - markAsIntellectualItem: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-intellectual-item', - }, - clientGeneratePk: false, - fetch: false, - }, - markAsLongMissing: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-long-missing', - }, - clientGeneratePk: false, - fetch: false, - }, - markAsRestricted: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-restricted', - }, - clientGeneratePk: false, - fetch: false, - }, - markAsUnavailable: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-unavailable', - }, - clientGeneratePk: false, - fetch: false, - }, - markAsUnknown: { - type: 'okapi', - POST: { - path: 'inventory/items/:{itemid}/mark-unknown', - }, - clientGeneratePk: false, - fetch: false, - }, - holdingsRecords: { - type: 'okapi', - path: 'holdings-storage/holdings/:{holdingsrecordid}', - tenant: '!{location.state.tenantTo}', - }, - instanceRecords: { - type: 'okapi', - path: 'inventory/instances/:{id}', - resourceShouldRefresh: true, - }, - servicePoints: { - type: 'okapi', - path: 'service-points', - records: 'servicepoints', - params: (_q, _p, _r, _l, props) => { - // Only one service point is of interest here: the SP used for the item's last check-in - // (if the item has a check-in). Iff that service point ID is found, add a query param - // to filter down to that one service point in the records returned. - const servicePointId = get(props.resources, 'itemsResource.records[0].lastCheckIn.servicePointId', ''); - const query = servicePointId && `id==${servicePointId}`; - return query ? { query } : {}; - }, - resourceShouldRefresh: true, - }, - staffMembers: { - type: 'okapi', - path: 'users', - records: 'users', - params: (_q, _p, _r, _l, props) => { - const staffMemberId = get(props.resources, 'itemsResource.records[0].lastCheckIn.staffMemberId', ''); - const query = staffMemberId && `id==${staffMemberId}`; - - return query ? { query } : null; - }, - resourceShouldRefresh: true, - }, - // return a count of the requests matching the given item and status - requests: { - type: 'okapi', - path: getRequestsPath, - records: 'requests', - PUT: { path: 'circulation/requests/%{requestOnItem.id}' }, - }, - openLoans: { - type: 'okapi', - path: 'circulation/loans', - params: { - query: 'status.name=="Open" and itemId==:{itemid}', - }, - records: 'loans', - }, - requestOnItem: {}, - tagSettings: { - type: 'okapi', - records: 'configs', - path: 'configurations/entries?query=(module==TAGS and configName==tags_enabled)', - }, - }); - - isLoading = () => { + render() { const { - resources: { - itemsResource, - holdingsRecords, - instanceRecords, - }, + stripes: { okapi }, + location: { state }, } = this.props; - if (!itemsResource?.hasLoaded || - !instanceRecords?.hasLoaded || - !holdingsRecords?.hasLoaded) { - return true; - } - - return false; - } - - render() { - if (this.isLoading()) { - return ; - } - return ( {data => ( )} diff --git a/src/routes/ViewHoldingRoute.js b/src/routes/ViewHoldingRoute.js index 7ad407a93..ff1b74871 100644 --- a/src/routes/ViewHoldingRoute.js +++ b/src/routes/ViewHoldingRoute.js @@ -1,5 +1,10 @@ import { useContext } from 'react'; -import { useParams } from 'react-router-dom'; +import { + useParams, + useLocation, +} from 'react-router-dom'; + +import { useStripes } from '@folio/stripes/core'; import { DataContext } from '../contexts'; import ViewHoldingsRecord from '../ViewHoldingsRecord'; @@ -7,10 +12,13 @@ import ViewHoldingsRecord from '../ViewHoldingsRecord'; const ViewHoldingRoute = () => { const { id: instanceId, holdingsrecordid } = useParams(); const referenceTables = useContext(DataContext); + const { okapi } = useStripes(); + const { state } = useLocation(); return ( diff --git a/src/routes/buildManifestObject.js b/src/routes/buildManifestObject.js index 291da5195..724325d77 100644 --- a/src/routes/buildManifestObject.js +++ b/src/routes/buildManifestObject.js @@ -2,7 +2,10 @@ import { get, } from 'lodash'; -import { makeQueryFunction } from '@folio/stripes/smart-components'; +import { + makeQueryFunction, + advancedSearchQueryToRows, +} from '@folio/stripes/smart-components'; import { CQL_FIND_ALL, fieldSearchConfigurations, @@ -21,38 +24,7 @@ const getQueryTemplateContributor = (queryValue) => `contributors.name==/string const getAdvancedSearchQueryTemplate = (queryIndex, matchOption) => fieldSearchConfigurations[queryIndex]?.[matchOption]; export const getAdvancedSearchTemplate = (queryValue) => { - const splitIntoRowsRegex = /(?=\sor\s|\sand\s|\snot\s)/g; - - // split will return array of strings: - // ['keyword==test', 'or issn=123', ...] - const rows = queryValue.split(splitIntoRowsRegex).map(i => i.trim()); - - return rows.map((match, index) => { - let bool = ''; - let query = match; - - // first row doesn't have a bool operator - if (index !== 0) { - bool = match.substr(0, match.indexOf(' ')); - query = match.substr(bool.length); - } - - const splitIndexAndQueryRegex = /([^=]+)(exactPhrase|containsAll|startsWith)(.+)/g; - - - const rowParts = [...query.matchAll(splitIndexAndQueryRegex)]?.[0] || []; - // eslint-disable-next-line no-unused-vars - const [, option, _match, value] = rowParts - .map(i => i.trim()) - .map(i => i.replaceAll('"', '')); - - return { - query: value, - bool, - searchOption: option, - match: _match, - }; - }).reduce((acc, row) => { + return advancedSearchQueryToRows(queryValue).reduce((acc, row) => { const rowTemplate = getAdvancedSearchQueryTemplate(row.searchOption, row.match); if (!rowTemplate) { diff --git a/src/views/ItemView.js b/src/views/ItemView.js index 30f224f74..d018c961d 100644 --- a/src/views/ItemView.js +++ b/src/views/ItemView.js @@ -5,6 +5,7 @@ import { isEmpty, values, sortBy, + flowRight, } from 'lodash'; import { parameterize } from 'inflected'; @@ -51,8 +52,11 @@ import { IntlConsumer, CalloutContext, checkIfUserInCentralTenant, + stripesConnect, } from '@folio/stripes/core'; +import { requestsStatusString } from '../Instance/ViewRequests/utils'; + import ModalContent from '../components/ModalContent'; import { ItemAcquisition } from '../Item/ViewItem/ItemAcquisition'; import { @@ -86,11 +90,152 @@ import { WarningMessage, AdministrativeNoteList, ItemViewSubheader, + PaneLoading, } from '../components'; export const requestStatusFiltersString = map(REQUEST_OPEN_STATUSES, requestStatus => `requestStatus.${requestStatus}`).join(','); class ItemView extends React.Component { + static manifest = Object.freeze({ + query: {}, + itemsResource: { + type: 'okapi', + path: 'inventory/items/:{itemid}', + POST: { path: 'inventory/items' }, + resourceShouldRefresh: true, + tenant: '!{tenantTo}', + }, + markItemAsWithdrawn: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-withdrawn', + }, + clientGeneratePk: false, + fetch: false, + }, + markItemAsMissing: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-missing', + }, + clientGeneratePk: false, + fetch: false, + }, + markAsInProcess: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-in-process', + }, + clientGeneratePk: false, + fetch: false, + }, + markAsInProcessNonRequestable: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-in-process-non-requestable', + }, + clientGeneratePk: false, + fetch: false, + }, + markAsIntellectualItem: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-intellectual-item', + }, + clientGeneratePk: false, + fetch: false, + }, + markAsLongMissing: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-long-missing', + }, + clientGeneratePk: false, + fetch: false, + }, + markAsRestricted: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-restricted', + }, + clientGeneratePk: false, + fetch: false, + }, + markAsUnavailable: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-unavailable', + }, + clientGeneratePk: false, + fetch: false, + }, + markAsUnknown: { + type: 'okapi', + POST: { + path: 'inventory/items/:{itemid}/mark-unknown', + }, + clientGeneratePk: false, + fetch: false, + }, + holdingsRecords: { + type: 'okapi', + path: 'holdings-storage/holdings/:{holdingsrecordid}', + tenant: '!{tenantTo}', + }, + instanceRecords: { + type: 'okapi', + path: 'inventory/instances/:{id}', + resourceShouldRefresh: true, + }, + servicePoints: { + type: 'okapi', + path: 'service-points', + records: 'servicepoints', + params: (_q, _p, _r, _l, props) => { + // Only one service point is of interest here: the SP used for the item's last check-in + // (if the item has a check-in). Iff that service point ID is found, add a query param + // to filter down to that one service point in the records returned. + const servicePointId = get(props.resources, 'itemsResource.records[0].lastCheckIn.servicePointId', ''); + const query = servicePointId && `id==${servicePointId}`; + return query ? { query } : {}; + }, + resourceShouldRefresh: true, + }, + staffMembers: { + type: 'okapi', + path: 'users', + records: 'users', + params: (_q, _p, _r, _l, props) => { + const staffMemberId = get(props.resources, 'itemsResource.records[0].lastCheckIn.staffMemberId', ''); + const query = staffMemberId && `id==${staffMemberId}`; + + return query ? { query } : null; + }, + resourceShouldRefresh: true, + }, + // return a count of the requests matching the given item and status + requests: { + type: 'okapi', + path: `circulation/requests?query=(itemId==:{itemid}) and status==(${requestsStatusString}) sortby requestDate desc&limit=1`, + records: 'requests', + PUT: { path: 'circulation/requests/%{requestOnItem.id}' }, + }, + openLoans: { + type: 'okapi', + path: 'circulation/loans', + params: { + query: 'status.name=="Open" and itemId==:{itemid}', + }, + records: 'loans', + }, + requestOnItem: {}, + tagSettings: { + type: 'okapi', + records: 'configs', + path: 'configurations/entries?query=(module==TAGS and configName==tags_enabled)', + }, + }); + static contextType = CalloutContext; constructor(props) { @@ -452,7 +597,23 @@ class ItemView extends React.Component { getEntity = () => this.props.resources.itemsResource.records[0]; getEntityTags = () => this.props.resources.itemsResource.records[0]?.tags?.tagList || []; + isLoading = () => { + const { + resources: { + instanceRecords, + itemsResource, + holdingsRecords, + }, + } = this.props; + + return !itemsResource?.hasLoaded || !instanceRecords?.hasLoaded || !holdingsRecords?.hasLoaded; + } + render() { + if (this.isLoading()) { + return ; + } + const { resources: { itemsResource, @@ -1536,15 +1697,24 @@ ItemView.propTypes = { }) }).isRequired, resources: PropTypes.shape({ - instanceRecords: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }), + instanceRecords: PropTypes.shape({ + hasLoaded: PropTypes.bool, + records: PropTypes.arrayOf(PropTypes.object), + }), loanTypes: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }), requests: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object), other: PropTypes.object, }), loans: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }), - itemsResource: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }), - holdingsRecords: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }), + itemsResource: PropTypes.shape({ + hasLoaded: PropTypes.bool, + records: PropTypes.arrayOf(PropTypes.object), + }), + holdingsRecords: PropTypes.shape({ + hasLoaded: PropTypes.bool, + records: PropTypes.arrayOf(PropTypes.object), + }), callNumberTypes: PropTypes.shape({ records: PropTypes.arrayOf(PropTypes.object) }), borrower: PropTypes.object, staffMembers: PropTypes.object, @@ -1596,4 +1766,7 @@ ItemView.propTypes = { history: PropTypes.object.isRequired, }; -export default withLocation(ItemView); +export default flowRight( + stripesConnect, + withLocation, +)(ItemView); diff --git a/src/views/ItemView.test.js b/src/views/ItemView.test.js index f65448ee8..13492c212 100644 --- a/src/views/ItemView.test.js +++ b/src/views/ItemView.test.js @@ -30,6 +30,7 @@ const stripesStub = { const resources = { holdingsRecords: { + hasLoaded: true, records: [ { permanentLocationId: 1, @@ -38,6 +39,7 @@ const resources = { ], }, itemsResource: { + hasLoaded: true, records: [ { id: 'item1', @@ -86,6 +88,7 @@ const resources = { ], }, instanceRecords: { + hasLoaded: true, records: [ { id: 1, diff --git a/test/jest/__mock__/index.js b/test/jest/__mock__/index.js index b75a3ae37..cb1422de0 100644 --- a/test/jest/__mock__/index.js +++ b/test/jest/__mock__/index.js @@ -6,7 +6,7 @@ import './stripesCore.mock'; import './stripesIcon.mock'; import './stripesSmartComponents.mock'; import './InstancePlugin.mock'; -import './quickMarc.mock'; +import './stripesMarcComponents.mock'; import './stripesComponents.mock'; import './reactBeautifulDnd.mock'; import './react-virtualized-auto-sizer'; diff --git a/test/jest/__mock__/quickMarc.mock.js b/test/jest/__mock__/quickMarc.mock.js deleted file mode 100644 index d38605f94..000000000 --- a/test/jest/__mock__/quickMarc.mock.js +++ /dev/null @@ -1,10 +0,0 @@ -import React from 'react'; - -jest.mock('@folio/quick-marc/src/QuickMarcView/QuickMarcView', () => ({ onClose, marcTitle }) => ( - <> - {marcTitle} - - -)); diff --git a/test/jest/__mock__/stripesMarcComponents.mock.js b/test/jest/__mock__/stripesMarcComponents.mock.js new file mode 100644 index 000000000..ea3bf1551 --- /dev/null +++ b/test/jest/__mock__/stripesMarcComponents.mock.js @@ -0,0 +1,13 @@ +import React from 'react'; + +jest.mock('@folio/stripes-marc-components', () => ({ + ...jest.requireActual('@folio/stripes-marc-components'), + MarcView: jest.fn(({ onClose, marcTitle }) => ( + <> + {marcTitle} + + + )), +}));