diff --git a/CHANGELOG.md b/CHANGELOG.md index 1bdfd3983..f2d326b23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Change history for ui-inventory +## [10.0.3](https://github.com/folio-org/ui-inventory/tree/v10.0.3) (2023-11-08) +[Full Changelog](https://github.com/folio-org/ui-inventory/compare/v10.0.2...v10.0.3) + +* ECS: Show info message when user in member tenant tries to view shared instance details without permission. Refs UIIN-2590. +* Show only local MARC Authorities when share local instance. Fixes UIIN-2647. +* Single instance export - MARC files sent to central tenant from member tenant. Fixes UIIN-2613. +* Fix misspelled Instance notes (all) advanced search query index. Fixes UIIN-2677. +* Switch from `=` to `==` operator when querying for `holdings-storage/holdings` by hrid. Fixes UIIN-2658. + ## [10.0.2](https://github.com/folio-org/ui-inventory/tree/v10.0.2) (2023-11-07) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v10.0.1...v10.0.2) diff --git a/package.json b/package.json index 4d423ba26..81a1276fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@folio/inventory", - "version": "10.0.2", + "version": "10.0.3", "description": "Inventory manager", "repository": "folio-org/ui-inventory", "publishConfig": { diff --git a/src/Instance/InstanceDetails/InstanceDetails.js b/src/Instance/InstanceDetails/InstanceDetails.js index 5f41c0c18..ee39f3d1f 100644 --- a/src/Instance/InstanceDetails/InstanceDetails.js +++ b/src/Instance/InstanceDetails/InstanceDetails.js @@ -19,7 +19,6 @@ import { PaneMenu, Row, MessageBanner, - Icon, } from '@folio/stripes/components'; import { InstanceTitle } from './InstanceTitle'; @@ -71,8 +70,6 @@ const InstanceDetails = forwardRef(({ tagsEnabled, userTenantPermissions, isShared, - isLoading, - isInstanceSharing, ...rest }, ref) => { const intl = useIntl(); @@ -86,15 +83,12 @@ const InstanceDetails = forwardRef(({ const accordionState = useMemo(() => getAccordionState(instance, accordions), [instance]); const [helperApp, setHelperApp] = useState(); - const isBasicPane = isInstanceSharing || isLoading; + const canCreateHoldings = stripes.hasPerm('ui-inventory.holdings.create'); const tags = instance?.tags?.tagList; const isUserInCentralTenant = checkIfUserInCentralTenant(stripes); - - const canCreateHoldings = stripes.hasPerm('ui-inventory.holdings.create'); + const isConsortialHoldingsVisible = instance?.shared || isInstanceShadowCopy(instance?.source); const detailsLastMenu = useMemo(() => { - if (isBasicPane) return null; - return ( { @@ -110,15 +104,9 @@ const InstanceDetails = forwardRef(({ } ); - }, [isBasicPane, tagsEnabled, tags, intl]); - const detailsActionMenu = useMemo( - () => (isBasicPane ? null : actionMenu), - [isBasicPane, actionMenu], - ); + }, [tagsEnabled, tags, intl]); const renderPaneTitle = () => { - if (isBasicPane) return intl.formatMessage({ id: 'ui-inventory.edit' }); - const isInstanceShared = Boolean(isShared || isInstanceShadowCopy(instance?.source)); return ( @@ -134,8 +122,6 @@ const InstanceDetails = forwardRef(({ }; const renderPaneSubtitle = () => { - if (isBasicPane) return null; - return ( { - if (isInstanceSharing) { - return ( -
- - - -
- ); - } - - if (isLoading) { - return ( -
- -
- ); - } - - const isConsortialHoldingsVisible = instance?.shared || isInstanceShadowCopy(instance?.source); - - return ( - <> + return ( + <> + } + paneTitle={renderPaneTitle()} + paneSub={renderPaneSubtitle()} + actionMenu={actionMenu} + lastMenu={detailsLastMenu} + dismissible + onClose={onClose} + defaultWidth="fill" + > @@ -293,25 +267,6 @@ const InstanceDetails = forwardRef(({ /> - - ); - }; - - return ( - <> - } - paneTitle={renderPaneTitle()} - paneSub={renderPaneSubtitle()} - actionMenu={detailsActionMenu} - lastMenu={detailsLastMenu} - dismissible - onClose={onClose} - defaultWidth="fill" - > - {renderDetails()} { helperApp && } @@ -326,16 +281,12 @@ InstanceDetails.propTypes = { tagsToggle: PropTypes.func, tagsEnabled: PropTypes.bool, userTenantPermissions: PropTypes.arrayOf(PropTypes.object), - isLoading: PropTypes.bool, - isInstanceSharing: PropTypes.bool, isShared: PropTypes.bool, }; InstanceDetails.defaultProps = { instance: {}, tagsEnabled: false, - isLoading: false, - isInstanceSharing: false, isShared: false, }; diff --git a/src/Instance/InstanceDetails/InstanceDetails.test.js b/src/Instance/InstanceDetails/InstanceDetails.test.js index e8cd22f7e..daf483dcd 100644 --- a/src/Instance/InstanceDetails/InstanceDetails.test.js +++ b/src/Instance/InstanceDetails/InstanceDetails.test.js @@ -112,13 +112,6 @@ describe('InstanceDetails', () => { expect(screen.getByText('Instance relationship (analytics and bound-with)')).toBeInTheDocument(); }); - it('should show a correct Warning message banner when instance sharing is in progress', () => { - renderInstanceDetails({ isInstanceSharing: true }); - - expect(screen.getByText('Sharing this local instance will take a few moments.' + - ' A success message and updated details will be displayed upon completion.')).toBeInTheDocument(); - }); - it('should show a correct Warning message banner when staff suppressed', () => { const staffSuppressedInstance = { ...instance, diff --git a/src/Instance/InstanceDetails/InstanceLoadingPane/InstanceLoadingPane.js b/src/Instance/InstanceDetails/InstanceLoadingPane/InstanceLoadingPane.js new file mode 100644 index 000000000..28d33c5eb --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceLoadingPane/InstanceLoadingPane.js @@ -0,0 +1,33 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; + +import { AppIcon } from '@folio/stripes/core'; +import { + Pane, + Icon, +} from '@folio/stripes/components'; + +const InstanceLoadingPane = ({ onClose }) => { + const intl = useIntl(); + + return ( + } + paneTitle={intl.formatMessage({ id: 'ui-inventory.edit' })} + dismissible + onClose={onClose} + defaultWidth="fill" + > + + + ); +}; + +InstanceLoadingPane.propTypes = { onClose: PropTypes.func.isRequired }; + +export default InstanceLoadingPane; diff --git a/src/Instance/InstanceDetails/InstanceLoadingPane/InstanceLoadingPane.test.js b/src/Instance/InstanceDetails/InstanceLoadingPane/InstanceLoadingPane.test.js new file mode 100644 index 000000000..a8f1e7bc8 --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceLoadingPane/InstanceLoadingPane.test.js @@ -0,0 +1,53 @@ +import { + fireEvent, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import '../../../../test/jest/__mock__'; + +import { Icon } from '@folio/stripes/components'; + +import { + renderWithIntl, + translationsProperties, +} from '../../../../test/jest/helpers'; + +import InstanceLoadingPane from './InstanceLoadingPane'; + +Icon.mockClear().mockImplementation(({ icon }) => {icon}); + +const mockOnClose = jest.fn(); + +const renderInstanceLoadingPane = () => { + const component = ; + + return renderWithIntl(component, translationsProperties); +}; + +describe('InstanceLoadingPane', () => { + it('should render loading spinner', () => { + renderInstanceLoadingPane(); + + expect(screen.getByText('spinner-ellipsis')).toBeInTheDocument(); + }); + + it('should render correct header', () => { + renderInstanceLoadingPane(); + + expect(screen.getByText('Edit')).toBeInTheDocument(); + }); + + it('should not render action menu', () => { + renderInstanceLoadingPane(); + + expect(screen.queryByRole('button', { name: 'Actions' })).not.toBeInTheDocument(); + }); + + it('should call onClose cb when pane is closed', () => { + renderInstanceLoadingPane(); + + fireEvent.click(screen.getByRole('button', { name: 'Close Edit' })); + + expect(mockOnClose).toHaveBeenCalled(); + }); +}); diff --git a/src/Instance/InstanceDetails/InstanceLoadingPane/index.js b/src/Instance/InstanceDetails/InstanceLoadingPane/index.js new file mode 100644 index 000000000..29e7c58ee --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceLoadingPane/index.js @@ -0,0 +1 @@ +export { default } from './InstanceLoadingPane'; diff --git a/src/Instance/InstanceDetails/InstanceWarningPane/InstanceWarningPane.js b/src/Instance/InstanceDetails/InstanceWarningPane/InstanceWarningPane.js new file mode 100644 index 000000000..8762ddf3e --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceWarningPane/InstanceWarningPane.js @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { useIntl } from 'react-intl'; + +import { AppIcon } from '@folio/stripes/core'; +import { + Pane, + MessageBanner, +} from '@folio/stripes/components'; + +const InstanceWarningPane = ({ + onClose, + messageBannerText, +}) => { + const intl = useIntl(); + + return ( + } + paneTitle={intl.formatMessage({ id: 'ui-inventory.edit' })} + dismissible + onClose={onClose} + defaultWidth="fill" + > + + {messageBannerText} + + + ); +}; + +InstanceWarningPane.propTypes = { + onClose: PropTypes.func.isRequired, + messageBannerText: PropTypes.oneOfType([PropTypes.element, PropTypes.string]).isRequired, +}; + +export default InstanceWarningPane; diff --git a/src/Instance/InstanceDetails/InstanceWarningPane/InstanceWarningPane.test.js b/src/Instance/InstanceDetails/InstanceWarningPane/InstanceWarningPane.test.js new file mode 100644 index 000000000..bb4603a2d --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceWarningPane/InstanceWarningPane.test.js @@ -0,0 +1,58 @@ +import { + fireEvent, + screen, +} from '@folio/jest-config-stripes/testing-library/react'; + +import '../../../../test/jest/__mock__'; + +import { Icon } from '@folio/stripes/components'; + +import { + renderWithIntl, + translationsProperties, +} from '../../../../test/jest/helpers'; + +import InstanceWarningPane from './InstanceWarningPane'; + +Icon.mockClear().mockImplementation(({ icon }) => {icon}); + +const mockOnClose = jest.fn(); + +const renderInstanceLoadingPane = (messageBannerText = 'warning text') => { + const component = ( + + ); + + return renderWithIntl(component, translationsProperties); +}; + +describe('InstanceWarningPane', () => { + it('should render warning banner', () => { + renderInstanceLoadingPane(); + + expect(screen.getByText('warning text')).toBeInTheDocument(); + }); + + it('should render correct header', () => { + renderInstanceLoadingPane(); + + expect(screen.getByText('Edit')).toBeInTheDocument(); + }); + + it('should not render action menu', () => { + renderInstanceLoadingPane(); + + expect(screen.queryByRole('button', { name: 'Actions' })).not.toBeInTheDocument(); + }); + + it('should call onClose cb when pane is closed', () => { + renderInstanceLoadingPane(); + + fireEvent.click(screen.getByRole('button', { name: 'Close Edit' })); + + expect(mockOnClose).toHaveBeenCalled(); + }); +}); diff --git a/src/Instance/InstanceDetails/InstanceWarningPane/index.js b/src/Instance/InstanceDetails/InstanceWarningPane/index.js new file mode 100644 index 000000000..ab6da1637 --- /dev/null +++ b/src/Instance/InstanceDetails/InstanceWarningPane/index.js @@ -0,0 +1 @@ +export { default } from './InstanceWarningPane'; diff --git a/src/Instance/InstanceDetails/index.js b/src/Instance/InstanceDetails/index.js index 065a56d8f..be1ebdbf8 100644 --- a/src/Instance/InstanceDetails/index.js +++ b/src/Instance/InstanceDetails/index.js @@ -11,6 +11,8 @@ export * from './InstanceTitle'; export * from './InstanceNotesView'; export * from './InstanceNewHolding'; +export { default as InstanceLoadingPane } from './InstanceLoadingPane'; +export { default as InstanceWarningPane } from './InstanceWarningPane'; export { default as SubInstanceList } from './SubInstanceList'; export { default as InstanceDetails } from './InstanceDetails'; export { default as SubInstanceGroup } from './SubInstanceGroup'; diff --git a/src/ViewInstance.js b/src/ViewInstance.js index f687c918a..146344a59 100644 --- a/src/ViewInstance.js +++ b/src/ViewInstance.js @@ -9,7 +9,6 @@ import { import { flowRight, isEmpty, - pick, } from 'lodash'; import { @@ -38,9 +37,11 @@ import { handleKeyCommand, isInstanceShadowCopy, isMARCSource, + getLinkedAuthorityIds, } from './utils'; import { - AUTHORITY_LINKED_FIELDS, + CONSORTIUM_PREFIX, + HTTP_RESPONSE_STATUS_CODES, indentifierTypeNames, INSTANCE_SHARING_STATUSES, layers, @@ -53,6 +54,8 @@ import { HoldingsListContainer, MoveItemsContext, InstanceDetails, + InstanceWarningPane, + InstanceLoadingPane, } from './Instance'; import { ActionItem, @@ -142,7 +145,6 @@ class ViewInstance extends React.Component { path: 'data-export/quick-export', throwErrors: false, clientGeneratePk: false, - tenant: '!{tenantId}', }, instanceRelationshipTypes: { type: 'okapi', @@ -167,6 +169,12 @@ class ViewInstance extends React.Component { accumulate: true, throwErrors: false, }, + authorities: { + type: 'okapi', + path: 'authority-storage/authorities', + accumulate: true, + throwErrors: false, + } }); constructor(props) { @@ -518,35 +526,32 @@ class ViewInstance extends React.Component { }); } - checkIfHasLinkedAuthorities = (instance = {}) => { - const linkedAuthorities = pick(this.props.selectedInstance, AUTHORITY_LINKED_FIELDS); + checkIfHasLinkedAuthorities = () => { + const { selectedInstance } = this.props; + const authorityIds = getLinkedAuthorityIds(selectedInstance); - const findLinkedAuthorities = authorities => { - const linkedAuthoritiesLength = AUTHORITY_LINKED_FIELDS.reduce((total, field) => { - const authoritiesAmount = authorities[field].filter(item => item.authorityId).length; - return total + authoritiesAmount; - }, 0); - - return { - hasLinkedAuthorities: !!linkedAuthoritiesLength, - linkedAuthoritiesLength, - }; - }; + if (isEmpty(authorityIds)) { + this.handleShareLocalInstance(selectedInstance); + return; + } - const { - hasLinkedAuthorities, - linkedAuthoritiesLength, - } = findLinkedAuthorities(linkedAuthorities); + this.props.mutator.authorities.GET({ + params: { + query: `id==(${authorityIds.join(' or ')})`, + } + }).then(({ authorities }) => { + const localAuthorities = authorities.filter(authority => !authority.source.startsWith(CONSORTIUM_PREFIX)); - if (hasLinkedAuthorities) { - this.setState({ - linkedAuthoritiesLength, - isShareLocalInstanceModalOpen: false, - isUnlinkAuthoritiesModalOpen: true, - }); - } else { - this.handleShareLocalInstance(instance); - } + if (localAuthorities.length) { + this.setState({ + linkedAuthoritiesLength: localAuthorities.length, + isShareLocalInstanceModalOpen: false, + isUnlinkAuthoritiesModalOpen: true, + }); + } else { + this.handleShareLocalInstance(selectedInstance); + } + }); } toggleCopyrightModal = () => { @@ -884,11 +889,18 @@ class ViewInstance extends React.Component { isCentralTenantPermissionsLoading, isShared, isLoading, + isError, + error, } = this.props; const { linkedAuthoritiesLength, isInstanceSharing, } = this.state; + + const isUserLacksPermToViewSharedInstance = isError + && error?.response?.status === HTTP_RESPONSE_STATUS_CODES.FORBIDDEN + && isShared; + const ci = makeConnectedInstance(this.props, stripes.logger); const instance = ci.instance(); @@ -925,6 +937,28 @@ class ViewInstance extends React.Component { const isInstanceLoading = isLoading || !instance || isCentralTenantPermissionsLoading; const keyInStorageToHoldingsAccsState = ['holdings']; + if (isUserLacksPermToViewSharedInstance) { + return ( + } + /> + ); + } + + if (isInstanceSharing) { + return ( + } + /> + ); + } + + if (isInstanceLoading) { + return ; + } + return ( {data => ( @@ -941,8 +975,6 @@ class ViewInstance extends React.Component { tagsEnabled={tagsEnabled} ref={this.accordionStatusRef} userTenantPermissions={this.state.userTenantPermissions} - isLoading={isInstanceLoading} - isInstanceSharing={isInstanceSharing} isShared={isShared} > { @@ -1001,7 +1033,7 @@ class ViewInstance extends React.Component { message={} confirmLabel={} onCancel={() => this.setState({ isShareLocalInstanceModalOpen: false })} - onConfirm={() => this.checkIfHasLinkedAuthorities(instance)} + onConfirm={this.checkIfHasLinkedAuthorities} /> instance) .mockImplementationOnce(() => {}); +Icon.mockClear().mockImplementation(({ children, icon }) => (children || {icon})); + ConfirmationModal.mockImplementation(({ open, onCancel, onConfirm, + heading, }) => (open ? (
- Confirmation modal + {heading}