diff --git a/CHANGELOG.md b/CHANGELOG.md index b20fadd84..74147f372 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,9 @@ * CI: Switch to centralized/shared workflow from https://github.com/folio-org/.github. Fixes UIIN-3218. * Set correct widths for Call number browse results columns. Fixes UIIN-3229. * Display actual instance state (shared or local) when user is using "Drag and drop" to move inventory records. Fixes UIIN-3185. +* Add Version history button and Version history pane to details view of Item. Refs UIIN-3172. +* Add Version history button and Version history pane to details view of Holdings. Refs UIIN-3171. +* Add Version history button and Version history pane to details view of Instance. Refs UIIN-3170. ## [12.0.12](https://github.com/folio-org/ui-inventory/tree/v12.0.12) (2025-01-27) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v12.0.11...v12.0.12) diff --git a/src/Instance/InstanceDetails/InstanceDetails.js b/src/Instance/InstanceDetails/InstanceDetails.js index cb800f56f..78e913532 100644 --- a/src/Instance/InstanceDetails/InstanceDetails.js +++ b/src/Instance/InstanceDetails/InstanceDetails.js @@ -26,7 +26,9 @@ import { Row, MessageBanner, PaneCloseLink, + Paneset, } from '@folio/stripes/components'; +import { VersionHistoryButton } from '@folio/stripes-acq-components'; import { InstanceTitle } from './InstanceTitle'; import { InstanceAdministrativeView } from './InstanceAdministrativeView'; @@ -42,6 +44,7 @@ import { InstanceRelationshipView } from './InstanceRelationshipView'; import { InstanceNewHolding } from './InstanceNewHolding'; import { InstanceAcquisition } from './InstanceAcquisition'; import HelperApp from '../../components/HelperApp'; +import { VersionHistory } from '../../views/VersionHistory'; import { DataContext } from '../../contexts'; import { ConsortialHoldings } from '../HoldingsList/consortium/ConsortialHoldings'; @@ -92,11 +95,13 @@ const InstanceDetails = forwardRef(({ const accordionState = useMemo(() => getAccordionState(instance, accordions), [instance]); const [helperApp, setHelperApp] = useState(); const [isAllExpanded, setIsAllExpanded] = useState(); + const [isVersionHistoryOpen, setIsVersionHistoryOpen] = useState(false); const canCreateHoldings = stripes.hasPerm('ui-inventory.holdings.create'); const tags = instance?.tags?.tagList; const isUserInCentralTenant = checkIfUserInCentralTenant(stripes); const isConsortialHoldingsVisible = instance?.shared || isInstanceShadowCopy(instance?.source); + const isUserInConsortium = isUserInConsortiumMode(stripes); const detailsLastMenu = useMemo(() => { return ( @@ -112,6 +117,7 @@ const InstanceDetails = forwardRef(({ /> ) } + setIsVersionHistoryOpen(true)} /> ); }, [tagsEnabled, tags, intl]); @@ -122,32 +128,35 @@ const InstanceDetails = forwardRef(({ const getEntity = () => instance; - const renderPaneTitle = () => { - const isInstanceShared = Boolean(isShared || isInstanceShadowCopy(instance?.source)); + const paneTitle = useMemo( + () => { + const isInstanceShared = Boolean(isShared || isInstanceShadowCopy(instance?.source)); - return ( - - ); - }; - - const renderPaneSubtitle = () => { - return ( - - ); - }; + } + ); + }, + [isShared, instance?.source, isUserInConsortium], + ); + const paneSubTitle = useMemo( + () => { + return ( + + ); + }, + [instance?.hrid, instance?.metadata?.updatedDate], + ); const onToggle = newState => { const isExpanded = Object.values(newState)[0]; @@ -161,14 +170,14 @@ const InstanceDetails = forwardRef(({ {...rest} data-test-instance-details appIcon={} - paneTitle={renderPaneTitle()} - paneSub={renderPaneSubtitle()} + paneTitle={paneTitle} + paneSub={paneSubTitle} actionMenu={actionMenu} firstMenu={( )} lastMenu={detailsLastMenu} @@ -301,6 +310,11 @@ const InstanceDetails = forwardRef(({ + {isVersionHistoryOpen && ( + setIsVersionHistoryOpen(false)} + /> + )} { helperApp && { expect(screen.queryByRole('button', { name: 'Consortial holdings' })).not.toBeInTheDocument(); }); }); + + describe('Version history component', () => { + let versionHistoryButton; + + beforeEach(async () => { + await act(async () => { renderInstanceDetails(); }); + + versionHistoryButton = screen.getByRole('button', { name: /version history/i }); + }); + + it('should render version history button', async () => { + expect(versionHistoryButton).toBeInTheDocument(); + }); + + describe('when click the button', () => { + it('should render version history pane', async () => { + await act(() => userEvent.click(versionHistoryButton)); + + expect(screen.getByRole('region', { name: /version history/i })).toBeInTheDocument(); + }); + }); + + describe('when click the close button', () => { + it('should hide the pane', async () => { + await act(() => userEvent.click(versionHistoryButton)); + + const versionHistoryPane = await screen.findByRole('region', { name: /version history/i }); + expect(versionHistoryPane).toBeInTheDocument(); + + const closeButton = await within(versionHistoryPane).findByRole('button', { name: /close/i }); + await act(() => userEvent.click(closeButton)); + + expect(screen.queryByRole('region', { name: /version history/i })).not.toBeInTheDocument(); + }); + }); + }); }); diff --git a/src/ViewHoldingsRecord.js b/src/ViewHoldingsRecord.js index 132d3611a..1698d10c5 100644 --- a/src/ViewHoldingsRecord.js +++ b/src/ViewHoldingsRecord.js @@ -33,6 +33,8 @@ import { collapseAllSections, expandAllSections, Callout, + PaneMenu, + Paneset, } from '@folio/stripes/components'; import { ViewMetaData, @@ -47,7 +49,10 @@ import { checkIfUserInCentralTenant, checkIfUserInMemberTenant, } from '@folio/stripes/core'; -import { FindLocation } from '@folio/stripes-acq-components'; +import { + FindLocation, + VersionHistoryButton, +} from '@folio/stripes-acq-components'; import { areAllFieldsEmpty, @@ -76,6 +81,7 @@ import { import HoldingAcquisitions from './Holding/ViewHolding/HoldingAcquisitions'; import HoldingReceivingHistory from './Holding/ViewHolding/HoldingReceivingHistory'; import HoldingBoundWith from './Holding/ViewHolding/HoldingBoundWith'; +import { VersionHistory } from './views/VersionHistory'; import css from './View.css'; @@ -152,6 +158,7 @@ class ViewHoldingsRecord extends React.Component { isUpdateOwnershipModalOpen: false, updateOwnershipData: {}, tenants: [], + isVersionHistoryOpen: false, }; this.cViewMetaData = props.stripes.connect(ViewMetaData); this.accordionStatusRef = createRef(); @@ -371,6 +378,10 @@ class ViewHoldingsRecord extends React.Component { this.setState({ hasLinkedLocalOrderLineModal: true }); } + openVersionHistory = () => { + this.setState({ isVersionHistoryOpen: true }); + } + callUpdateOwnership = () => { const { stripes: { okapi }, @@ -932,51 +943,57 @@ class ViewHoldingsRecord extends React.Component { isWithinScope={checkScope} scope={document.body} > - } - paneTitle={intl.formatMessage({ - id: 'ui-inventory.holdingsPaneTitle', - }, { - location: + + } + paneTitle={intl.formatMessage({ + id: 'ui-inventory.holdingsPaneTitle', + }, { + location: holdingsEffectiveLocation?.isActive ? holdingsEffectiveLocation?.name : intl.formatMessage( { id: 'ui-inventory.inactive.paneTitle' }, { location: holdingsEffectiveLocation?.name } ), - callNumber: holdingsRecord?.callNumber, - })} - paneSub={intl.formatMessage({ - id: 'ui-inventory.instanceRecordSubtitle', - }, { - hrid: holdingsRecord?.hrid, - updatedDate: getDate(holdingsRecord?.metadata?.updatedDate), - })} - dismissible - onClose={this.onClose} - actionMenu={this.getPaneHeaderActionMenu} - > - - - - - {instance?.title} - - {(instance?.publication && instance?.publication.length > 0) && - - . - - {instance?.publication[0].publisher} - {instance?.publication[0].dateOfPublication ? `, ${instance?.publication[0].dateOfPublication}` : ''} - - + callNumber: holdingsRecord?.callNumber, + })} + paneSub={intl.formatMessage({ + id: 'ui-inventory.instanceRecordSubtitle', + }, { + hrid: holdingsRecord?.hrid, + updatedDate: getDate(holdingsRecord?.metadata?.updatedDate), + })} + dismissible + onClose={this.onClose} + actionMenu={this.getPaneHeaderActionMenu} + lastMenu={( + + + + )} + > + + + + + {instance?.title} + + {(instance?.publication && instance?.publication.length > 0) && + + . + + {instance?.publication[0].publisher} + {instance?.publication[0].dateOfPublication ? `, ${instance?.publication[0].dateOfPublication}` : ''} + + } - - -
- { + +
+
+ { itemCount === 0 && <> @@ -993,141 +1010,141 @@ class ViewHoldingsRecord extends React.Component {
} - - - - { + + + + { itemCount === 0 && effectiveLocationDisplay } - - - - - - - - - - - - - - - - - - - - } - > - - - - {holdingsRecord.discoverySuppress && } - - -
- - - }> - {checkIfElementIsEmpty(administrativeData.holdingsHrid)} - {Boolean(administrativeData.holdingsHrid) && } - - - - } - value={checkIfElementIsEmpty(holdingsSourceName)} - /> - - - } - value={checkIfElementIsEmpty(formerHoldingsIdValue)} - /> - - - - - } - value={checkIfElementIsEmpty(administrativeData.holdingsType)} - /> - - - - - + + + + + + + + + + + + + + + + + + + } + > + + + + {holdingsRecord.discoverySuppress && } + + +
+ + + }> + {checkIfElementIsEmpty(administrativeData.holdingsHrid)} + {Boolean(administrativeData.holdingsHrid) && } + + + + } + value={checkIfElementIsEmpty(holdingsSourceName)} + /> + + + } + value={checkIfElementIsEmpty(formerHoldingsIdValue)} + /> + + + + + } + value={checkIfElementIsEmpty(administrativeData.holdingsType)} + /> + + + + + this.refLookup(referenceTables.statisticalCodeTypes, this.refLookup(referenceTables.statisticalCodes, get(x, ['codeId'])).statisticalCodeTypeId).name || noValue, - 'Statistical code name': + 'Statistical code name': x => this.refLookup(referenceTables.statisticalCodes, get(x, ['codeId'])).name || noValue, - }} - ariaLabel={intl.formatMessage({ id: 'ui-inventory.statisticalCodes' })} - containerRef={ref => { this.resultsList = ref; }} - /> - - - - - - - -
- - } - > - - - - - - - -
- - - } - value={checkIfElementIsEmpty(locationAccordion.permanent?.name)} - subValue={(!locationAccordion.permanent?.isActive) && + }} + ariaLabel={intl.formatMessage({ id: 'ui-inventory.statisticalCodes' })} + containerRef={ref => { this.resultsList = ref; }} + /> + + + + + + + +
+ + } + > + + + + + + + +
+ + + } + value={checkIfElementIsEmpty(locationAccordion.permanent?.name)} + subValue={(!locationAccordion.permanent?.isActive) && } - /> - - - } - value={checkIfElementIsEmpty(locationAccordion.temporary?.name)} - subValue={(locationAccordion.temporary && + /> + + + } + value={checkIfElementIsEmpty(locationAccordion.temporary?.name)} + subValue={(locationAccordion.temporary && !locationAccordion.temporary?.isActive) && } - data-test-id="temporary-location" - /> - - - { + data-test-id="temporary-location" + /> + +
+ { itemCount === 0 && @@ -1135,184 +1152,190 @@ class ViewHoldingsRecord extends React.Component { } - - - } - value={checkIfElementIsEmpty(locationAccordion.shelvingOrder)} - /> - - - } - value={checkIfElementIsEmpty(locationAccordion.shelvingTitle)} - /> - - - - - - - - - - - - } - value={checkIfElementIsEmpty(locationAccordion.copyNumber)} - /> - - - } - value={checkIfElementIsEmpty(locationAccordion.callNumberType)} - /> - - - } - value={checkIfElementIsEmpty(locationAccordion.callNumberPrefix)} - /> - - - } - value={checkIfElementIsEmpty(locationAccordion.callNumber)} - /> - - - } - value={checkIfElementIsEmpty(locationAccordion.callNumberSuffix)} - /> - - - - - } - > - } - value={checkIfElementIsEmpty(holdingsDetails.numberOfItems)} - /> - {holdingsDetailsTables(intl).map(item => ( - - { this.resultsList = ref; }} - /> + + + } + value={checkIfElementIsEmpty(locationAccordion.shelvingOrder)} + /> + + + } + value={checkIfElementIsEmpty(locationAccordion.shelvingTitle)} + /> + + + + + + + + - ))} - - - } - value={checkIfElementIsEmpty(holdingsDetails.illPolicy)} - /> - - - } - value={checkIfElementIsEmpty(holdingsDetails.digitizationPolicy)} - /> - - - } - value={checkIfElementIsEmpty(holdingsDetails.retentionPolicy)} - /> - - - - - {tagsEnabled && ( + + + } + value={checkIfElementIsEmpty(locationAccordion.copyNumber)} + /> + + + } + value={checkIfElementIsEmpty(locationAccordion.callNumberType)} + /> + + + } + value={checkIfElementIsEmpty(locationAccordion.callNumberPrefix)} + /> + + + } + value={checkIfElementIsEmpty(locationAccordion.callNumber)} + /> + + + } + value={checkIfElementIsEmpty(locationAccordion.callNumberSuffix)} + /> + + + + + } + > + } + value={checkIfElementIsEmpty(holdingsDetails.numberOfItems)} + /> + {holdingsDetailsTables(intl).map(item => ( + + { this.resultsList = ref; }} + /> + + ))} + + + } + value={checkIfElementIsEmpty(holdingsDetails.illPolicy)} + /> + + + } + value={checkIfElementIsEmpty(holdingsDetails.digitizationPolicy)} + /> + + + } + value={checkIfElementIsEmpty(holdingsDetails.retentionPolicy)} + /> + + + + + {tagsEnabled && ( - )} - - } - > - {layoutNotes(holdingsNotes)} - - - } - > - this.refLookup(referenceTables.electronicAccessRelationships, get(x, ['relationshipId'])).name || noValue, - 'URI': x => { - const uri = x?.uri; - - return uri - ? ( - - {uri} - - ) - : noValue; - }, - 'Link text': x => get(x, ['linkText']) || noValue, - 'Materials specified': x => get(x, ['materialsSpecification']) || noValue, - 'URL public note': x => get(x, ['publicNote']) || noValue, - }} - ariaLabel={intl.formatMessage({ id: 'ui-inventory.electronicAccess' })} - containerRef={ref => { this.resultsList = ref; }} - /> - + )} + + } + > + {layoutNotes(holdingsNotes)} + + + } + > + this.refLookup(referenceTables.electronicAccessRelationships, get(x, ['relationshipId'])).name || noValue, + 'URI': x => { + const uri = x?.uri; + + return uri + ? ( + + {uri} + + ) + : noValue; + }, + 'Link text': x => get(x, ['linkText']) || noValue, + 'Materials specified': x => get(x, ['materialsSpecification']) || noValue, + 'URL public note': x => get(x, ['publicNote']) || noValue, + }} + ariaLabel={intl.formatMessage({ id: 'ui-inventory.electronicAccess' })} + containerRef={ref => { this.resultsList = ref; }} + /> + - + - + - + - -
-
+ + +
+ {this.state.isVersionHistoryOpen && ( + this.setState({ isVersionHistoryOpen: false })} + /> + )} + @@ -1357,7 +1380,6 @@ ViewHoldingsRecord.propTypes = { history: PropTypes.object.isRequired, id: PropTypes.string.isRequired, holdingsrecordid: PropTypes.string.isRequired, - paneWidth: PropTypes.string, referenceTables: PropTypes.object.isRequired, mutator: PropTypes.shape({ instances1: PropTypes.shape({ diff --git a/src/ViewHoldingsRecord.test.js b/src/ViewHoldingsRecord.test.js index 44633e6f4..fd80a1bd1 100644 --- a/src/ViewHoldingsRecord.test.js +++ b/src/ViewHoldingsRecord.test.js @@ -12,6 +12,7 @@ import { waitFor, within, } from '@folio/jest-config-stripes/testing-library/react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; import { checkIfUserInMemberTenant } from '@folio/stripes/core'; import { FindLocation } from '@folio/stripes-acq-components'; @@ -574,4 +575,40 @@ describe('ViewHoldingsRecord actions', () => { }); }); }); + + describe('Version history component', () => { + let versionHistoryButton; + + beforeEach(async () => { + await act(async () => { renderViewHoldingsRecord(); }); + + versionHistoryButton = screen.getByRole('button', { name: /version history/i }); + }); + + it('should render version history button', async () => { + expect(versionHistoryButton).toBeInTheDocument(); + }); + + describe('when click the button', () => { + it('should render version history pane', async () => { + await userEvent.click(versionHistoryButton); + + expect(screen.getByRole('region', { name: /version history/i })).toBeInTheDocument(); + }); + }); + + describe('when click the close button', () => { + it('should hide the pane', async () => { + await userEvent.click(versionHistoryButton); + + const versionHistoryPane = await screen.findByRole('region', { name: /version history/i }); + expect(versionHistoryPane).toBeInTheDocument(); + + const closeButton = await within(versionHistoryPane).findByRole('button', { name: /close/i }); + await userEvent.click(closeButton); + + expect(screen.queryByRole('region', { name: /version history/i })).not.toBeInTheDocument(); + }); + }); + }); }); diff --git a/src/views/ItemView.js b/src/views/ItemView.js index afd01fdec..e63dd454e 100644 --- a/src/views/ItemView.js +++ b/src/views/ItemView.js @@ -51,6 +51,7 @@ import { MenuSection, NoValue, TextLink, + PaneMenu, } from '@folio/stripes/components'; import { @@ -68,6 +69,7 @@ import { useOkapiKy, } from '@folio/stripes/core'; +import { VersionHistoryButton } from '@folio/stripes-acq-components'; import { requestsStatusString } from '../Instance/ViewRequests/utils'; import ModalContent from '../components/ModalContent'; @@ -116,6 +118,7 @@ import { useHoldingMutation, useUpdateOwnership, } from '../hooks'; +import { VersionHistory } from './VersionHistory'; export const requestStatusFiltersString = map(REQUEST_OPEN_STATUSES, requestStatus => `requestStatus.${requestStatus}`).join(','); @@ -160,6 +163,7 @@ const ItemView = props => { const [updateOwnershipData, setUpdateOwnershipData] = useState({}); const [tenants, setTenants] = useState([]); const [targetTenant, setTargetTenant] = useState({}); + const [isVersionHistoryOpen, setIsSetVersionHistoryOpen] = useState(false); const intl = useIntl(); const calloutContext = useContext(CalloutContext); @@ -620,6 +624,10 @@ const ItemView = props => { const temporaryHoldingsLocation = locationsById[holdingsRecord?.temporaryLocationId]; const tagsEnabled = !tagSettings?.records?.length || tagSettings?.records?.[0]?.value === 'true'; + const openVersionHistory = useCallback(() => { + setIsSetVersionHistoryOpen(true); + }, []); + const refLookup = (referenceTable, id) => { const ref = (referenceTable && id) ? referenceTable.find(record => record.id === id) : {}; @@ -983,7 +991,8 @@ const ItemView = props => { { dismissible onClose={onCloseViewItem} actionMenu={getActionMenu} + lastMenu={( + + + + )} > { + {isVersionHistoryOpen && ( + setIsSetVersionHistoryOpen(false)} + /> + )} ); diff --git a/src/views/ItemView.test.js b/src/views/ItemView.test.js index 4eccb8da3..d95da6df7 100644 --- a/src/views/ItemView.test.js +++ b/src/views/ItemView.test.js @@ -8,6 +8,7 @@ import { fireEvent, within, } from '@folio/jest-config-stripes/testing-library/react'; +import userEvent from '@folio/jest-config-stripes/testing-library/user-event'; import { runAxeTest } from '@folio/stripes-testing'; import '../../test/jest/__mock__'; @@ -292,6 +293,41 @@ describe('ItemView', () => { expect(mockPush).toHaveBeenCalled(); }); }); + + describe('Version history component', () => { + let versionHistoryButton; + + beforeEach(() => { + const { container } = renderWithIntl(, translationsProperties); + + versionHistoryButton = container.querySelector('#version-history-btn'); + }); + + it('should render version history button', () => { + expect(versionHistoryButton).toBeInTheDocument(); + }); + + describe('when click the button', () => { + it('should render version history pane', async () => { + await userEvent.click(versionHistoryButton); + expect(screen.getByRole('region', { name: /version history/i })).toBeInTheDocument(); + }); + }); + + describe('when click the close button', () => { + it('should hide the pane', async () => { + await userEvent.click(versionHistoryButton); + + const versionHistoryPane = await screen.findByRole('region', { name: /version history/i }); + expect(versionHistoryPane).toBeInTheDocument(); + + const closeButton = await within(versionHistoryPane).findByRole('button', { name: /close/i }); + await userEvent.click(closeButton); + + expect(screen.queryByRole('region', { name: /version history/i })).not.toBeInTheDocument(); + }); + }); + }); }); describe('action menu', () => { diff --git a/src/views/VersionHistory/VersionHistory.js b/src/views/VersionHistory/VersionHistory.js new file mode 100644 index 000000000..205d77b4d --- /dev/null +++ b/src/views/VersionHistory/VersionHistory.js @@ -0,0 +1,40 @@ +import PropTypes from 'prop-types'; + +import { + VersionHistoryPane, + VersionViewContextProvider, +} from '@folio/stripes-acq-components'; + +const VersionHistory = ({ onClose }) => { + // TODO: remove once API for inventory version history is implemented and pass currentVersion and versionId to props + // set mocked data to avoid unit tests warnings, because these props are required + const currentVersion = 'mockedVersion'; + const versionId = 'mockedVersionId'; + + return ( + + {}} + snapshotPath="" + labelsMap={{}} + versions={[]} + hiddenFields={[]} + /> + + ); +}; + +VersionHistory.propTypes = { + onClose: PropTypes.func, +}; + + +export default VersionHistory; diff --git a/src/views/VersionHistory/index.js b/src/views/VersionHistory/index.js new file mode 100644 index 000000000..01d26d546 --- /dev/null +++ b/src/views/VersionHistory/index.js @@ -0,0 +1 @@ +export { default as VersionHistory } from './VersionHistory';