From 54f4d9c0406944c8ae0553f3068763d8e2de72c3 Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Thu, 9 May 2024 18:34:52 +0200 Subject: [PATCH 1/4] UIIN-2896 Inventory app: Define and implement shortcut key for editing a quickMARC bib record --- CHANGELOG.md | 1 + src/ViewHoldingsRecord.js | 6 ++ src/ViewInstance.js | 6 ++ src/components/ViewSource/ViewSource.js | 93 ++++++++++++++++++------- src/index.js | 33 +++++---- translations/ui-inventory/en.json | 3 +- 6 files changed, 101 insertions(+), 41 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6a86f615..7d8ba7e68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ * Jest/RTL: Cover MoveHoldingContext component with unit tests. Refs UIIN-2664. * Use consolidated locations endpoint to fetch all locations when in central tenant context. Refs UIIN-2811. * Change label of eye-readable call number search option in holdings/items. Refs UIIN-2797. +* Inventory app: Define and implement shortcut key for editing a quickMARC bib record. Refs UIIN-2896. ## [11.0.4](https://github.com/folio-org/ui-inventory/tree/v11.0.4) (2024-04-30) [Full Changelog](https://github.com/folio-org/ui-inventory/compare/v11.0.3...v11.0.4) diff --git a/src/ViewHoldingsRecord.js b/src/ViewHoldingsRecord.js index dc1c9cff3..577877b62 100644 --- a/src/ViewHoldingsRecord.js +++ b/src/ViewHoldingsRecord.js @@ -689,6 +689,12 @@ 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.handleEditInQuickMarc(); + }), + }, { name: 'expandAllSections', handler: (e) => expandAllSections(e, this.accordionStatusRef), diff --git a/src/ViewInstance.js b/src/ViewInstance.js index 2d64d331d..c16f3e0bb 100644 --- a/src/ViewInstance.js +++ b/src/ViewInstance.js @@ -1066,6 +1066,12 @@ 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')) this.editInstanceMarc(); + }), + }, { name: 'duplicateRecord', handler: handleKeyCommand(() => { diff --git a/src/components/ViewSource/ViewSource.js b/src/components/ViewSource/ViewSource.js index 57fab5307..b77b68516 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,10 @@ import { import { useGoBack } from '../../common/hooks'; -import { isUserInConsortiumMode } from '../../utils'; +import { + isUserInConsortiumMode, + handleKeyCommand, +} from '../../utils'; import MARC_TYPES from './marcTypes'; import styles from './ViewSource.css'; @@ -33,6 +44,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 +64,30 @@ 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}`; + + const searchParams = new URLSearchParams(location.search); + + searchParams.delete('relatedRecordVersion'); + searchParams.append('shared', instance.shared?.toString()); + + history.push({ + pathname, + search: searchParams.toString(), + }); + }, [isHoldingsRecord]); + + const shortcuts = useMemo(() => [ + { + name: 'editMARC', + handler: handleKeyCommand(() => { + if (stripes.hasPerm('ui-quick-marc.quick-marc-editor.all')) redirectToMARCEdit(); + }), + }, + ]); + useEffect(() => { setIsMarcLoading(true); @@ -94,32 +131,38 @@ const ViewSource = ({ ); return ( -
- - - - } - /> - {isPrintAvailable && isShownPrintPopup && ( - +
+ + + + } /> - )} -
+ {isPrintAvailable && isShownPrintPopup && ( + + )} +
+ ); }; diff --git a/src/index.js b/src/index.js index 7ae10f30f..c0e53e418 100644 --- a/src/index.js +++ b/src/index.js @@ -62,20 +62,23 @@ 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(4, 0, { + label: (), + name: 'editMARC', + shortcut: 'ctrl+shift+e', + }); + keyboardShortcuts.splice(11, 0, { + label: , + name: 'NEXT_SUBFIELD', + shortcut: 'Ctrl + ]', + }, + { + label: , + name: 'PREV_SUBFIELD', + shortcut: 'Ctrl + [', + }); useEffect(() => { return () => { @@ -112,7 +115,7 @@ const InventoryRouting = (props) => { - + Date: Fri, 10 May 2024 16:33:02 +0200 Subject: [PATCH 2/4] UIIN-2896 added tests for new edit marc record shortcuts --- src/ViewHoldingsRecord.test.js | 20 ++++++++++-- src/ViewInstance.test.js | 15 +++++++++ src/components/ViewSource/ViewSource.test.js | 32 +++++++++++++++++++- 3 files changed, 64 insertions(+), 3 deletions(-) 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.test.js b/src/ViewInstance.test.js index 93a64c975..68fd6897e 100644 --- a/src/ViewInstance.test.js +++ b/src/ViewInstance.test.js @@ -1162,4 +1162,19 @@ describe('ViewInstance', () => { expect(spyOnexpandAllSections).toBeCalled(); }); }); + + describe('when using an editMARC shortcut', () => { + it('should redirect to marc edit page', () => { + renderViewInstance({ + isShared: true, + }); + + 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.test.js b/src/components/ViewSource/ViewSource.test.js index a719b9381..43c83f303 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: 'shared=false', + }); + }); + }); }); From c48a893a4a76f66c5434e8b46811bcc8aca3108f Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Mon, 13 May 2024 10:15:08 +0200 Subject: [PATCH 3/4] UIIN-2896 disable edit MARC record for source FOLIO --- src/ViewHoldingsRecord.js | 6 +++++- src/ViewInstance.js | 7 ++++++- src/index.js | 5 ++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/ViewHoldingsRecord.js b/src/ViewHoldingsRecord.js index 577877b62..0cea981f7 100644 --- a/src/ViewHoldingsRecord.js +++ b/src/ViewHoldingsRecord.js @@ -692,7 +692,11 @@ class ViewHoldingsRecord extends React.Component { { name: 'editMARC', handler: handleKeyCommand(() => { - if (stripes.hasPerm('ui-quick-marc.quick-marc-editor.all')) this.handleEditInQuickMarc(); + if (!stripes.hasPerm('ui-quick-marc.quick-marc-editor.all') || !this.isMARCSource()) { + return; + } + + this.handleEditInQuickMarc(); }), }, { diff --git a/src/ViewInstance.js b/src/ViewInstance.js index c16f3e0bb..7acdfde0d 100644 --- a/src/ViewInstance.js +++ b/src/ViewInstance.js @@ -1042,6 +1042,7 @@ class ViewInstance extends React.Component { onCopy, updateLocation, canUseSingleRecordImport, + selectedInstance, } = this.props; const { linkedAuthoritiesLength, @@ -1069,7 +1070,11 @@ class ViewInstance extends React.Component { { name: 'editMARC', handler: handleKeyCommand(() => { - if (stripes.hasPerm('ui-quick-marc.quick-marc-editor.all')) this.editInstanceMarc(); + if (!stripes.hasPerm('ui-quick-marc.quick-marc-editor.all') || !isMARCSource(selectedInstance.source)) { + return; + } + + this.editInstanceMarc(); }), }, { diff --git a/src/index.js b/src/index.js index c0e53e418..7f6b70021 100644 --- a/src/index.js +++ b/src/index.js @@ -64,12 +64,11 @@ const InventoryRouting = (props) => { const keyboardShortcuts = [...defaultKeyboardShortcuts]; - keyboardShortcuts.splice(4, 0, { + keyboardShortcuts.splice(10, 0, { label: (), name: 'editMARC', shortcut: 'ctrl+shift+e', - }); - keyboardShortcuts.splice(11, 0, { + }, { label: , name: 'NEXT_SUBFIELD', shortcut: 'Ctrl + ]', From a9243f1f28007a7d82676a70ffda0b2712885b3f Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Mon, 13 May 2024 13:32:30 +0200 Subject: [PATCH 4/4] UIIN-2896 Added a util to redirect to edit MARC page --- src/ViewInstance.js | 12 ++-------- src/ViewInstance.test.js | 15 ++++++++----- src/components/ViewSource/ViewSource.js | 13 +++-------- src/components/ViewSource/ViewSource.test.js | 2 +- src/utils.js | 15 +++++++++++++ src/utils.test.js | 23 ++++++++++++++++++++ 6 files changed, 54 insertions(+), 26 deletions(-) diff --git a/src/ViewInstance.js b/src/ViewInstance.js index 7acdfde0d..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 = () => { diff --git a/src/ViewInstance.test.js b/src/ViewInstance.test.js index 68fd6897e..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' }); @@ -1165,9 +1165,14 @@ describe('ViewInstance', () => { describe('when using an editMARC shortcut', () => { it('should redirect to marc edit page', () => { - renderViewInstance({ - isShared: true, - }); + const selectedInstance = { + ...instances[0], + shared: true, + }; + + StripesConnectedInstance.prototype.instance.mockImplementation(() => selectedInstance); + + renderViewInstance({ selectedInstance }); fireEvent.click(screen.getByRole('button', { name: 'editMARC' })); diff --git a/src/components/ViewSource/ViewSource.js b/src/components/ViewSource/ViewSource.js index b77b68516..ee01f043c 100644 --- a/src/components/ViewSource/ViewSource.js +++ b/src/components/ViewSource/ViewSource.js @@ -29,6 +29,7 @@ import { useGoBack } from '../../common/hooks'; import { isUserInConsortiumMode, handleKeyCommand, + redirectToMarcEditPage, } from '../../utils'; import MARC_TYPES from './marcTypes'; @@ -68,15 +69,7 @@ const ViewSource = ({ const urlId = isHoldingsRecord ? `${instanceId}/${holdingsRecordId}` : instanceId; const pathname = `/inventory/quick-marc/edit-${isHoldingsRecord ? 'holdings' : 'bib'}/${urlId}`; - const searchParams = new URLSearchParams(location.search); - - searchParams.delete('relatedRecordVersion'); - searchParams.append('shared', instance.shared?.toString()); - - history.push({ - pathname, - search: searchParams.toString(), - }); + redirectToMarcEditPage(pathname, instance, location, history); }, [isHoldingsRecord]); const shortcuts = useMemo(() => [ @@ -86,7 +79,7 @@ const ViewSource = ({ if (stripes.hasPerm('ui-quick-marc.quick-marc-editor.all')) redirectToMARCEdit(); }), }, - ]); + ], [stripes, redirectToMARCEdit]); useEffect(() => { setIsMarcLoading(true); diff --git a/src/components/ViewSource/ViewSource.test.js b/src/components/ViewSource/ViewSource.test.js index 43c83f303..cb5dec039 100644 --- a/src/components/ViewSource/ViewSource.test.js +++ b/src/components/ViewSource/ViewSource.test.js @@ -179,7 +179,7 @@ describe('ViewSource', () => { expect(mockPush).toHaveBeenLastCalledWith({ pathname: `/inventory/quick-marc/edit-bib/${mockInstance.id}`, - search: 'shared=false', + search: '', }); }); }); diff --git a/src/utils.js b/src/utils.js index 115fd4b99..f7f2a32cd 100644 --- a/src/utils.js +++ b/src/utils.js @@ -915,3 +915,18 @@ export const getTemplateForSelectedFromBrowseRecord = (queryParams, queryIndex, return null; }; + +export const redirectToMarcEditPage = (pathname, instance, location, history) => { + const searchParams = new URLSearchParams(location.search); + + searchParams.delete('relatedRecordVersion'); + + if (instance.shared) { + searchParams.append('shared', instance.shared.toString()); + } + + history.push({ + pathname, + search: searchParams.toString(), + }); +}; diff --git a/src/utils.test.js b/src/utils.test.js index 4e4c3938a..f2ecf8510 100644 --- a/src/utils.test.js +++ b/src/utils.test.js @@ -15,6 +15,7 @@ import { switchAffiliation, setRecordForDeletion, parseEmptyFormValue, + redirectToMarcEditPage, } from './utils'; import { CONTENT_TYPE_HEADER, @@ -278,3 +279,25 @@ describe('parseEmptyFormValue', () => { expect(parseEmptyFormValue(value)).toEqual(undefined); }); }); + +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', + }); + }); +});