diff --git a/CHANGELOG.md b/CHANGELOG.md index a91fd77b1..1d482ea44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * Add callout noting user's active affiliation when it changes after selecting holding or item. Refs UIIN-2831, UIIN-2872. * Jest/RTL: Cover LocationSelectionWithCheck components with unit tests. Refs UIIN-2670. * Populate Acquisitions accordion on instance when central ordering is active. Refs UIIN-2793. +* Inventory app: Define and implement shortcut key for editing a quickMARC bib record. Refs UIIN-2896. * *BREAKING* Added a new `stripes-inventory-components` dependency. Move some utils to that module. Refs UIIN-2910. ## [11.0.4](https://github.com/folio-org/ui-inventory/tree/v11.0.4) (2024-04-30) diff --git a/src/ViewHoldingsRecord.js b/src/ViewHoldingsRecord.js index dc1c9cff3..0cea981f7 100644 --- a/src/ViewHoldingsRecord.js +++ b/src/ViewHoldingsRecord.js @@ -689,6 +689,16 @@ class ViewHoldingsRecord extends React.Component { if (stripes.hasPerm('ui-inventory.holdings.edit')) this.onEditHolding(); }), }, + { + name: 'editMARC', + handler: handleKeyCommand(() => { + if (!stripes.hasPerm('ui-quick-marc.quick-marc-editor.all') || !this.isMARCSource()) { + return; + } + + this.handleEditInQuickMarc(); + }), + }, { name: 'expandAllSections', handler: (e) => expandAllSections(e, this.accordionStatusRef), diff --git a/src/ViewHoldingsRecord.test.js b/src/ViewHoldingsRecord.test.js index cd820998a..e64f19245 100644 --- a/src/ViewHoldingsRecord.test.js +++ b/src/ViewHoldingsRecord.test.js @@ -43,10 +43,11 @@ const spyOncollapseAllSections = jest.spyOn(require('@folio/stripes/components') const spyOnexpandAllSections = jest.spyOn(require('@folio/stripes/components'), 'expandAllSections'); const mockData = jest.fn().mockResolvedValue({ id: 'testId' }); +const mockGoTo = jest.fn(); const defaultProps = { id: 'id', - goTo: jest.fn(), + goTo: mockGoTo, holdingsrecordid: 'holdingId', referenceTables: { holdingsSources: [{ id: 'sourceId', name: 'MARC' }], @@ -57,7 +58,12 @@ const defaultProps = { resources: { holdingsRecords: { records: [ - { sourceId: 'sourceId', temporaryLocationId: 'inactiveLocation' } + { + sourceId: 'sourceId', + temporaryLocationId: 'inactiveLocation', + id: 'holdingId', + _version: 1, + } ], }, instances1: { records: [{ id: 'instanceId' }], hasLoaded: true }, @@ -270,4 +276,14 @@ describe('ViewHoldingsRecord actions', () => { expect(spyOnexpandAllSections).toHaveBeenCalled(); }); }); + + describe('when using an editMARC shortcut', () => { + it('should redirect to marc edit page', async () => { + await act(async () => { renderViewHoldingsRecord(); }); + + fireEvent.click(screen.getByRole('button', { name: 'editMARC' })); + + expect(mockGoTo).toHaveBeenLastCalledWith(`/inventory/quick-marc/edit-holdings/instanceId/${defaultProps.holdingsrecordid}?%2F=&relatedRecordVersion=1`); + }); + }); }); diff --git a/src/ViewInstance.js b/src/ViewInstance.js index 2d64d331d..2a62d2215 100644 --- a/src/ViewInstance.js +++ b/src/ViewInstance.js @@ -39,6 +39,7 @@ import { isMARCSource, getLinkedAuthorityIds, setRecordForDeletion, + redirectToMarcEditPage, } from './utils'; import { CONSORTIUM_PREFIX, @@ -339,21 +340,12 @@ class ViewInstance extends React.Component { history, location, stripes, - isShared, } = this.props; const ci = makeConnectedInstance(this.props, stripes.logger); const instance = ci.instance(); - const searchParams = new URLSearchParams(location.search); - - searchParams.delete('relatedRecordVersion'); - searchParams.append('shared', isShared.toString()); - - history.push({ - pathname: `/inventory/quick-marc/${page}/${instance.id}`, - search: searchParams.toString(), - }); + redirectToMarcEditPage(`/inventory/quick-marc/${page}/${instance.id}`, instance, location, history); }; editInstanceMarc = () => { @@ -1042,6 +1034,7 @@ class ViewInstance extends React.Component { onCopy, updateLocation, canUseSingleRecordImport, + selectedInstance, } = this.props; const { linkedAuthoritiesLength, @@ -1066,6 +1059,16 @@ class ViewInstance extends React.Component { if (stripes.hasPerm('ui-inventory.instance.edit')) this.onClickEditInstance(); }), }, + { + name: 'editMARC', + handler: handleKeyCommand(() => { + if (!stripes.hasPerm('ui-quick-marc.quick-marc-editor.all') || !isMARCSource(selectedInstance.source)) { + return; + } + + this.editInstanceMarc(); + }), + }, { name: 'duplicateRecord', handler: handleKeyCommand(() => { diff --git a/src/ViewInstance.test.js b/src/ViewInstance.test.js index 93a64c975..567c7ee04 100644 --- a/src/ViewInstance.test.js +++ b/src/ViewInstance.test.js @@ -836,7 +836,7 @@ describe('ViewInstance', () => { renderViewInstance(); const expectedValue = { pathname: `/inventory/quick-marc/edit-bib/${defaultProp.selectedInstance.id}`, - search: 'filters=test1&query=test2&sort=test3&qindex=test&shared=false', + search: 'filters=test1&query=test2&sort=test3&qindex=test', }; fireEvent.click(screen.getByRole('button', { name: 'Actions' })); const button = screen.getByRole('button', { name: 'Edit MARC bibliographic record' }); @@ -929,7 +929,7 @@ describe('ViewInstance', () => { renderViewInstance({ isShared: false }); const expectedValue = { pathname: `/inventory/quick-marc/duplicate-bib/${defaultProp.selectedInstance.id}`, - search: 'filters=test1&query=test2&sort=test3&qindex=test&shared=false', + search: 'filters=test1&query=test2&sort=test3&qindex=test', }; fireEvent.click(screen.getByRole('button', { name: 'Actions' })); const button = screen.getByRole('button', { name: 'Derive new MARC bibliographic record' }); @@ -1162,4 +1162,24 @@ describe('ViewInstance', () => { expect(spyOnexpandAllSections).toBeCalled(); }); }); + + describe('when using an editMARC shortcut', () => { + it('should redirect to marc edit page', () => { + const selectedInstance = { + ...instances[0], + shared: true, + }; + + StripesConnectedInstance.prototype.instance.mockImplementation(() => selectedInstance); + + renderViewInstance({ selectedInstance }); + + fireEvent.click(screen.getByRole('button', { name: 'editMARC' })); + + expect(mockPush).toHaveBeenLastCalledWith({ + pathname: `/inventory/quick-marc/edit-bib/${instance.id}`, + search: 'filters=test1&query=test2&sort=test3&qindex=test&shared=true', + }); + }); + }); }); diff --git a/src/components/ViewSource/ViewSource.js b/src/components/ViewSource/ViewSource.js index 57fab5307..ee01f043c 100644 --- a/src/components/ViewSource/ViewSource.js +++ b/src/components/ViewSource/ViewSource.js @@ -1,13 +1,21 @@ import React, { useState, useEffect, + useMemo, + useCallback, } from 'react'; +import { + useLocation, + useHistory, +} from 'react-router-dom'; import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; import { Button, LoadingView, + HasCommand, + checkScope, } from '@folio/stripes/components'; import { useStripes } from '@folio/stripes/core'; import { @@ -18,7 +26,11 @@ import { import { useGoBack } from '../../common/hooks'; -import { isUserInConsortiumMode } from '../../utils'; +import { + isUserInConsortiumMode, + handleKeyCommand, + redirectToMarcEditPage, +} from '../../utils'; import MARC_TYPES from './marcTypes'; import styles from './ViewSource.css'; @@ -33,6 +45,8 @@ const ViewSource = ({ marcType, }) => { const stripes = useStripes(); + const location = useLocation(); + const history = useHistory(); const [isShownPrintPopup, setIsShownPrintPopup] = useState(false); const openPrintPopup = () => setIsShownPrintPopup(true); const closePrintPopup = () => setIsShownPrintPopup(false); @@ -51,6 +65,22 @@ const ViewSource = ({ const [marc, setMarc] = useState(); const [isMarcLoading, setIsMarcLoading] = useState(true); + const redirectToMARCEdit = useCallback(() => { + const urlId = isHoldingsRecord ? `${instanceId}/${holdingsRecordId}` : instanceId; + const pathname = `/inventory/quick-marc/edit-${isHoldingsRecord ? 'holdings' : 'bib'}/${urlId}`; + + redirectToMarcEditPage(pathname, instance, location, history); + }, [isHoldingsRecord]); + + const shortcuts = useMemo(() => [ + { + name: 'editMARC', + handler: handleKeyCommand(() => { + if (stripes.hasPerm('ui-quick-marc.quick-marc-editor.all')) redirectToMARCEdit(); + }), + }, + ], [stripes, redirectToMARCEdit]); + useEffect(() => { setIsMarcLoading(true); @@ -94,32 +124,38 @@ const ViewSource = ({ ); return ( -
- - - - } - /> - {isPrintAvailable && isShownPrintPopup && ( - +
+ + + + } /> - )} -
+ {isPrintAvailable && isShownPrintPopup && ( + + )} +
+ ); }; diff --git a/src/components/ViewSource/ViewSource.test.js b/src/components/ViewSource/ViewSource.test.js index a719b9381..cb5dec039 100644 --- a/src/components/ViewSource/ViewSource.test.js +++ b/src/components/ViewSource/ViewSource.test.js @@ -1,5 +1,8 @@ import React from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { + BrowserRouter as Router, + useHistory, +} from 'react-router-dom'; import { act } from 'react-dom/test-utils'; import { @@ -18,6 +21,12 @@ import { CONSORTIUM_PREFIX } from '../../constants'; import MARC_TYPES from './marcTypes'; jest.mock('../../common/hooks/useGoBack', () => jest.fn()); +jest.mock('react-router-dom', () => ({ + ...jest.requireActual('react-router-dom'), + useHistory: jest.fn().mockReturnValue({ + push: jest.fn(), + }), +})); const mutator = { marcRecord: { @@ -26,6 +35,7 @@ const mutator = { }; const mockGoBack = jest.fn(); +const mockPush = jest.fn(); const mockInstance = { id: 'instance-id', title: 'Instance title', @@ -49,6 +59,9 @@ const getViewSource = (props = {}) => ( describe('ViewSource', () => { beforeEach(() => { + useHistory.mockClear().mockReturnValue({ + push: mockPush, + }); useGoBack.mockReturnValue(mockGoBack); }); @@ -153,4 +166,21 @@ describe('ViewSource', () => { expect(screen.getByText('Local MARC bibliographic record')).toBeInTheDocument(); }); }); + + describe('when using an editMARC shortcut', () => { + beforeEach(async () => { + await act(async () => { + await renderWithIntl(getViewSource(), translations); + }); + }); + + it('should redirect to marc edit page', () => { + fireEvent.click(screen.getByRole('button', { name: 'editMARC' })); + + expect(mockPush).toHaveBeenLastCalledWith({ + pathname: `/inventory/quick-marc/edit-bib/${mockInstance.id}`, + search: '', + }); + }); + }); }); diff --git a/src/index.js b/src/index.js index 7ae10f30f..7f6b70021 100644 --- a/src/index.js +++ b/src/index.js @@ -62,20 +62,22 @@ const InventoryRouting = (props) => { const [isShortcutsModalOpen, setIsShortcutsModalOpen] = useState(false); const { showSettings, match: { path } } = props; - const keyboardShortcuts = [ - ...defaultKeyboardShortcuts.slice(0, 9), - { - label: , - name: 'NEXT_SUBFIELD', - shortcut: 'Ctrl + ]', - }, - { - label: , - name: 'PREV_SUBFIELD', - shortcut: 'Ctrl + [', - }, - ...defaultKeyboardShortcuts.slice(9), - ]; + const keyboardShortcuts = [...defaultKeyboardShortcuts]; + + keyboardShortcuts.splice(10, 0, { + label: (), + name: 'editMARC', + shortcut: 'ctrl+shift+e', + }, { + label: , + name: 'NEXT_SUBFIELD', + shortcut: 'Ctrl + ]', + }, + { + label: , + name: 'PREV_SUBFIELD', + shortcut: 'Ctrl + [', + }); useEffect(() => { return () => { @@ -112,7 +114,7 @@ const InventoryRouting = (props) => { - + { + const searchParams = new URLSearchParams(location.search); + + searchParams.delete('relatedRecordVersion'); + + if (instance.shared) { + searchParams.append('shared', instance.shared.toString()); + } + + history.push({ + pathname, + search: searchParams.toString(), + }); +}; + export const sendCalloutOnAffiliationChange = (stripes, tenantId, callout) => { if (tenantId && stripes.okapi.tenant !== tenantId) { const name = stripes.user.user.tenants.find(tenant => tenant.id === tenantId)?.name; diff --git a/src/utils.test.js b/src/utils.test.js index 43051e9f8..c84afba7e 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -15,6 +15,7 @@ import { switchAffiliation, setRecordForDeletion, parseEmptyFormValue, + redirectToMarcEditPage, sendCalloutOnAffiliationChange, } from './utils'; import { @@ -280,6 +281,28 @@ describe('parseEmptyFormValue', () => { }); }); +describe('redirectToMarcEditPage', () => { + it('should call history.push with correct arguments', () => { + const pathname = 'some-pathname'; + const instance = { + shared: true, + }; + const location = { + search: '?someValue=test&relatedRecordVersion=1', + }; + const history = { + push: jest.fn(), + }; + + redirectToMarcEditPage(pathname, instance, location, history); + + expect(history.push).toHaveBeenCalledWith({ + pathname, + search: 'someValue=test&shared=true', + }); + }); +}); + describe('sendCalloutOnAffiliationChange', () => { const callout = { sendCallout: jest.fn(), diff --git a/translations/ui-inventory/en.json b/translations/ui-inventory/en.json index 145613f56..e1da4d564 100644 --- a/translations/ui-inventory/en.json +++ b/translations/ui-inventory/en.json @@ -876,5 +876,6 @@ "info.shelvingOrder": "This field is the normalized form of the call number which determines how the call number is sorted while browsing.", "shortcut.nextSubfield": "quickMARC only: Move to the next subfield in a text box", - "shortcut.prevSubfield": "quickMARC only: Move to the previous subfield in a text box" + "shortcut.prevSubfield": "quickMARC only: Move to the previous subfield in a text box", + "shortcut.editMARC": "Edit MARC record" }