diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c63e769c..e841e4d8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,7 @@ * Use `==` for exact phrase search in Advanced Search for all full-text and term fields. Refs UIIN-2612. * Provide an instance `tenantId` to the PO line form when creating an order from the instance. Refs UIIN-2614. * Bump @folio/stripes-acq-components dependency version to 5.0.0. Refs UIIN-2620. +* ECS: Check when sharing instance with source=MARC is complete before re-fetching it. Refs UIIN-2605. ## [9.4.12](https://github.com/folio-org/ui-inventory/tree/v9.4.12) (2023-09-21) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v9.4.11...v9.4.12) diff --git a/src/Instance/InstanceDetails/InstanceDetails.js b/src/Instance/InstanceDetails/InstanceDetails.js index 36e7b8d1d..36df56a26 100644 --- a/src/Instance/InstanceDetails/InstanceDetails.js +++ b/src/Instance/InstanceDetails/InstanceDetails.js @@ -19,6 +19,7 @@ import { PaneMenu, Row, MessageBanner, + Icon, } from '@folio/stripes/components'; import { InstanceTitle } from './InstanceTitle'; @@ -36,9 +37,17 @@ import { InstanceNewHolding } from './InstanceNewHolding'; import { InstanceAcquisition } from './InstanceAcquisition'; import HelperApp from '../../components/HelperApp'; -import { getAccordionState } from './utils'; import { DataContext } from '../../contexts'; import { ConsortialHoldings } from './ConsortialHoldings'; +import { + getAccordionState, + getPublishingInfo, +} from './utils'; +import { + getDate, + isInstanceShadowCopy, + isUserInConsortiumMode, +} from '../../utils'; const accordions = { administrative: 'acc01', @@ -57,21 +66,22 @@ const accordions = { const InstanceDetails = forwardRef(({ children, instance, - paneTitle, - paneSubtitle, onClose, actionMenu, tagsEnabled, + isShared, + isLoading, ...rest }, ref) => { - const stripes = useStripes(); const intl = useIntl(); + const stripes = useStripes(); const location = useLocation(); const searchParams = new URLSearchParams(location.search); const referenceData = useContext(DataContext); const accordionState = useMemo(() => getAccordionState(instance, accordions), [instance]); const [helperApp, setHelperApp] = useState(); + const tags = instance?.tags?.tagList; const isUserInCentralTenant = checkIfUserInCentralTenant(stripes); @@ -93,14 +103,61 @@ const InstanceDetails = forwardRef(({ ); }, [tagsEnabled, tags, intl]); + if (isLoading) { + return ( + } + dismissible + onClose={onClose} + > +
+ +
+
+ ); + } + + const renderPaneTitle = () => { + const isInstanceShared = Boolean(isShared || isInstanceShadowCopy(instance?.source)); + + return ( + + ); + }; + + const renderPaneSubtitle = () => { + return ( + + ); + }; + return ( <> } - paneTitle={paneTitle} - paneSub={paneSubtitle} + paneTitle={renderPaneTitle()} + paneSub={renderPaneSubtitle()} dismissible onClose={onClose} actionMenu={actionMenu} @@ -232,15 +289,17 @@ InstanceDetails.propTypes = { actionMenu: PropTypes.func, onClose: PropTypes.func.isRequired, instance: PropTypes.object, - paneTitle: PropTypes.object, - paneSubtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), tagsToggle: PropTypes.func, tagsEnabled: PropTypes.bool, + isLoading: PropTypes.bool, + isShared: PropTypes.bool, }; InstanceDetails.defaultProps = { instance: {}, tagsEnabled: false, + isLoading: false, + isShared: false, }; export default InstanceDetails; diff --git a/src/Instance/InstanceDetails/utils.js b/src/Instance/InstanceDetails/utils.js index 1d9b3bbbb..3b8082097 100644 --- a/src/Instance/InstanceDetails/utils.js +++ b/src/Instance/InstanceDetails/utils.js @@ -20,7 +20,7 @@ export const getPublishingInfo = instance => { return undefined; }; -export const getAccordionState = (instance, accordions) => { +export const getAccordionState = (instance = {}, accordions = {}) => { const instanceData = pick( instance, ['hrid', 'source', 'catalogedDate', 'statusId', 'statusUpdatedDate', 'modeOfIssuanceId', 'statisticalCodeIds'], diff --git a/src/ViewInstance.js b/src/ViewInstance.js index 7ce69c470..676336d2c 100644 --- a/src/ViewInstance.js +++ b/src/ViewInstance.js @@ -12,15 +12,12 @@ import { } from 'lodash'; import { - AppIcon, Pluggable, stripesConnect, checkIfUserInMemberTenant, checkIfUserInCentralTenant, } from '@folio/stripes/core'; import { - Pane, - Icon, MenuSection, Callout, checkScope, @@ -34,17 +31,16 @@ import ViewHoldingsRecord from './ViewHoldingsRecord'; import makeConnectedInstance from './ConnectedInstance'; import withLocation from './withLocation'; import InstancePlugin from './components/InstancePlugin'; -import { getPublishingInfo } from './Instance/InstanceDetails/utils'; import { - getDate, handleKeyCommand, isInstanceShadowCopy, isMARCSource, - isUserInConsortiumMode, } from './utils'; import { indentifierTypeNames, + INSTANCE_SHARING_STATUSES, layers, + OKAPI_TENANT_HEADER, REQUEST_OPEN_STATUSES, } from './constants'; import { DataContext } from './contexts'; @@ -176,6 +172,7 @@ class ViewInstance extends React.Component { this.log = logger.log.bind(logger); this.state = { + isLoading: false, marcRecord: null, findInstancePluginOpened: false, isItemsMovement: false, @@ -187,6 +184,7 @@ class ViewInstance extends React.Component { instancesQuickExportInProgress: false, }; this.instanceId = null; + this.intervalId = null; this.cViewHoldingsRecord = this.props.stripes.connect(ViewHoldingsRecord); this.calloutRef = createRef(); @@ -244,6 +242,7 @@ class ViewInstance extends React.Component { componentWillUnmount() { this.props.mutator.allInstanceItems.reset(); + clearInterval(this.intervalId); } getMARCRecord = () => { @@ -414,6 +413,48 @@ class ViewInstance extends React.Component { this.setState({ isImportRecordModalOpened: false }); } + checkInstanceSharingProgress = ({ sourceTenantId, instanceIdentifier }) => { + return this.props.mutator.shareInstance.GET({ + params: { sourceTenantId, instanceIdentifier }, + headers: { + [OKAPI_TENANT_HEADER]: this.props.centralTenantId, + 'Content-Type': 'application/json', + ...(this.props.okapi.token && { 'X-Okapi-Token': this.props.okapi.token }), + }, + }); + } + + waitForInstanceSharingComplete = ({ sourceTenantId, instanceIdentifier, instanceTitle }) => { + return new Promise((resolve, reject) => { + this.intervalId = setInterval(() => { + const onError = error => { + this.calloutRef.current.sendCallout({ + type: 'error', + message: , + }); + clearInterval(this.intervalId); + reject(error); + }; + const onSuccess = response => { + const sharingStatus = response?.sharingInstances[0]?.status; + + if (sharingStatus === INSTANCE_SHARING_STATUSES.COMPLETE) { + clearInterval(this.intervalId); + resolve(response); + } + + if (sharingStatus === INSTANCE_SHARING_STATUSES.ERROR) { + onError(response); + } + }; + + this.checkInstanceSharingProgress({ sourceTenantId, instanceIdentifier }) + .then(onSuccess) + .catch(onError); + }, 2000); + }); + } + handleShareLocalInstance = (instance = {}) => { const centralTenantId = this.props.centralTenantId; const sourceTenantId = this.props.okapi.tenant; @@ -425,21 +466,28 @@ class ViewInstance extends React.Component { instanceIdentifier, targetTenantId: centralTenantId, }) + .then(async () => { + this.setState({ + isShareLocalInstanceModalOpen: false, + isLoading: true + }); + + await this.waitForInstanceSharingComplete({ sourceTenantId, instanceIdentifier, instanceTitle }); + }) .then(async () => { await this.props.refetchInstance(); + this.setState({ isLoading: false }); this.calloutRef.current.sendCallout({ type: 'success', message: , }); }) .catch(() => { - this.calloutRef.current.sendCallout({ + this.setState({ isShareLocalInstanceModalOpen: false }); + this.calloutRef.current?.sendCallout({ type: 'error', message: , }); - }) - .finally(() => { - this.setState({ isShareLocalInstanceModalOpen: false }); }); } @@ -765,26 +813,6 @@ class ViewInstance extends React.Component { ); }; - renderPaneTitle = (instance) => { - const { - stripes, - isShared, - } = this.props; - - const isInstanceShared = Boolean(isShared || isInstanceShadowCopy(instance?.source)); - - return ( - - ); - }; - render() { const { match: { params: { id, holdingsrecordid, itemid } }, @@ -792,12 +820,11 @@ class ViewInstance extends React.Component { okapi, onCopy, onClose, - paneWidth, tagsEnabled, updateLocation, canUseSingleRecordImport, - intl, isCentralTenantPermissionsLoading, + isShared, } = this.props; const ci = makeConnectedInstance(this.props, stripes.logger); const instance = ci.instance(); @@ -832,26 +859,7 @@ class ViewInstance extends React.Component { handler: (e) => collapseAllSections(e, this.accordionStatusRef), }, ]; - - if (!instance || isCentralTenantPermissionsLoading) { - return ( - } - dismissible - onClose={onClose} - > -
- -
-
- ); - } + const isInstanceLoading = this.state.isLoading || !instance || isCentralTenantPermissionsLoading; return ( @@ -863,21 +871,13 @@ class ViewInstance extends React.Component { > - } onClose={onClose} actionMenu={this.createActionMenuGetter(instance, data)} instance={instance} tagsEnabled={tagsEnabled} ref={this.accordionStatusRef} + isLoading={isInstanceLoading} + isShared={isShared} > { (!holdingsrecordid && !itemid) ? @@ -900,7 +900,7 @@ class ViewInstance extends React.Component { {this.state.afterCreate && } + message={} /> } @@ -986,11 +986,13 @@ ViewInstance.propTypes = { GET: PropTypes.func.isRequired, reset: PropTypes.func.isRequired, }).isRequired, - shareInstance: PropTypes.shape({ POST: PropTypes.func.isRequired }).isRequired, + shareInstance: PropTypes.shape({ + POST: PropTypes.func.isRequired, + GET: PropTypes.func.isRequired, + }).isRequired, }), onClose: PropTypes.func, onCopy: PropTypes.func, - paneWidth: PropTypes.string.isRequired, resources: PropTypes.shape({ allInstanceItems: PropTypes.object.isRequired, allInstanceHoldings: PropTypes.object.isRequired, diff --git a/src/ViewInstance.test.js b/src/ViewInstance.test.js index e98da3da8..bf9529ff9 100644 --- a/src/ViewInstance.test.js +++ b/src/ViewInstance.test.js @@ -175,7 +175,10 @@ const defaultProp = { GET: jest.fn(() => Promise.resolve([])), reset: jest.fn() }, - shareInstance: { POST: jest.fn() }, + shareInstance: { + POST: jest.fn(), + GET: jest.fn(() => Promise.resolve({ sharingInstances: [{ status: 'COMPLETE' }] })), + }, }, onClose: mockonClose, onCopy: jest.fn(), @@ -259,6 +262,7 @@ describe('ViewInstance', () => { jest.clearAllMocks(); StripesConnectedInstance.prototype.instance.mockImplementation(() => instance); checkIfUserInCentralTenant.mockReturnValue(false); + useStripes().hasInterface.mockReturnValue(true); }); it('should display action menu items', () => { renderViewInstance(); @@ -273,7 +277,7 @@ describe('ViewInstance', () => { describe('instance header', () => { describe('for non-consortia users', () => { it('should render instance title, publisher, and publication date', () => { - defaultProp.stripes.hasInterface.mockReturnValue(false); + useStripes().hasInterface.mockReturnValue(false); const { getByText } = renderViewInstance(); const expectedTitle = 'Instance • #youthaction • Information Age Publishing, Inc. • 2015'; diff --git a/src/constants.js b/src/constants.js index ec12052fa..9e0f42c7a 100644 --- a/src/constants.js +++ b/src/constants.js @@ -677,3 +677,9 @@ export const CONSORTIUM_PREFIX = 'CONSORTIUM-'; export const OKAPI_TENANT_HEADER = 'X-Okapi-Tenant'; export const DEFAULT_ITEM_TABLE_SORTBY_FIELD = 'barcode'; + +export const INSTANCE_SHARING_STATUSES = { + COMPLETE: 'COMPLETE', + ERROR: 'ERROR', + IN_PROGRESS: 'IN_PROGRESS', +};