From cdd7b541ad8c0e3314f5f4b70ce15dc0024c70f2 Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Wed, 16 Oct 2024 13:56:35 +0200 Subject: [PATCH] UIQM-652 eliminate page refresh when using save and keep ediing [WIP] (#740) * UIQM-652 eliminate page refresh when using save and keep ediing * UIQM-652 added onInputFocus to prop types * UIQM-652 update tests for updating related record version * UIQM-652 fix issues --- src/QuickMarcEditor/QuickMarcEditWrapper.js | 6 +-- .../QuickMarcEditWrapper.test.js | 15 ++++---- src/QuickMarcEditor/QuickMarcEditor.js | 29 ++++++--------- .../QuickMarcEditorContainer.js | 30 ++++----------- .../QuickMarcEditorContainer.test.js | 37 ------------------- .../QuickMarcEditorRows.js | 3 ++ .../QuickMarcEditorRows.test.js | 1 + .../QuickMarcContext/QuickMarcContext.js | 7 ++++ .../useFocusFirstFieldWithError.js | 4 +- test/jest/helpers/harness.js | 7 +++- 10 files changed, 49 insertions(+), 90 deletions(-) diff --git a/src/QuickMarcEditor/QuickMarcEditWrapper.js b/src/QuickMarcEditor/QuickMarcEditWrapper.js index 838d1ea1..85531437 100644 --- a/src/QuickMarcEditor/QuickMarcEditWrapper.js +++ b/src/QuickMarcEditor/QuickMarcEditWrapper.js @@ -80,7 +80,7 @@ const QuickMarcEditWrapper = ({ const showCallout = useShowCallout(); const location = useLocation(); const [httpError, setHttpError] = useState(null); - const { validationErrorsRef } = useContext(QuickMarcContext); + const { validationErrorsRef, relatedRecordVersion } = useContext(QuickMarcContext); const { token, locale } = stripes.okapi; const isRequestToCentralTenantFromMember = applyCentralTenantInHeaders(location, stripes, marcType); @@ -203,7 +203,7 @@ const QuickMarcEditWrapper = ({ formValuesToHydrate._actionType = 'edit'; formValuesToHydrate.relatedRecordVersion = marcType === MARC_TYPES.AUTHORITY ? instance._version - : new URLSearchParams(location.search).get('relatedRecordVersion'); + : relatedRecordVersion; const formValuesToSave = hydrateMarcRecord(formValuesToHydrate); @@ -241,7 +241,6 @@ const QuickMarcEditWrapper = ({ marcType, mutator, linksCount, - location, prepareForSubmit, actualizeLinks, centralTenantId, @@ -250,6 +249,7 @@ const QuickMarcEditWrapper = ({ updateMarcRecord, isRequestToCentralTenantFromMember, validationErrorsRef, + relatedRecordVersion, ]); return ( diff --git a/src/QuickMarcEditor/QuickMarcEditWrapper.test.js b/src/QuickMarcEditor/QuickMarcEditWrapper.test.js index 18c9926c..d7176c3c 100644 --- a/src/QuickMarcEditor/QuickMarcEditWrapper.test.js +++ b/src/QuickMarcEditor/QuickMarcEditWrapper.test.js @@ -361,9 +361,10 @@ const renderQuickMarcEditWrapper = ({ instance, mutator, marcType = MARC_TYPES.BIB, + quickMarcContext, ...props }) => (render( - +
{ isLoading: false, }); - useLocation.mockReturnValue({ - search: 'relatedRecordVersion=1', - }); + useLocation.mockReturnValue({}); useValidate.mockReturnValue({ validate: mockValidateFetch, @@ -481,13 +480,16 @@ describe('Given QuickMarcEditWrapper', () => { it('should show on save message and redirect on load page', async () => { const mockOnSave = jest.fn(); - const { getByText } = renderQuickMarcEditWrapper({ + const { getByRole } = renderQuickMarcEditWrapper({ instance, mutator, onSave: mockOnSave, + quickMarcContext: { + relatedRecordVersion: 1, + }, }); - await act(async () => { fireEvent.click(getByText('stripes-acq-components.FormFooter.save')); }); + await act(async () => { fireEvent.click(getByRole('button', { name: 'stripes-acq-components.FormFooter.save' })); }); expect(mutator.quickMarcEditInstance.GET).toHaveBeenCalled(); expect(mockUpdateMarcRecord).toHaveBeenCalled(); @@ -620,7 +622,6 @@ describe('Given QuickMarcEditWrapper', () => { 'parsedRecordDtoId': '1bf159d9-4da8-4c3f-9aac-c83e68356bbf', 'parsedRecordId': '1bf159d9-4da8-4c3f-9aac-c83e68356bbf', 'records': undefined, - 'relatedRecordVersion': '1', 'suppressDiscovery': false, 'updateInfo': { 'recordState': 'NEW', diff --git a/src/QuickMarcEditor/QuickMarcEditor.js b/src/QuickMarcEditor/QuickMarcEditor.js index ad6fea44..18901adb 100644 --- a/src/QuickMarcEditor/QuickMarcEditor.js +++ b/src/QuickMarcEditor/QuickMarcEditor.js @@ -6,10 +6,7 @@ import React, { useEffect, useContext, } from 'react'; -import { - useHistory, - useLocation, -} from 'react-router'; +import { useLocation } from 'react-router'; import { FormSpy } from 'react-final-form'; import PropTypes from 'prop-types'; import { FormattedMessage, useIntl } from 'react-intl'; @@ -119,7 +116,6 @@ const QuickMarcEditor = ({ const stripes = useStripes(); const intl = useIntl(); const formValues = getState().values; - const history = useHistory(); const location = useLocation(); const showCallout = useShowCallout(); const [records, setRecords] = useState([]); @@ -131,8 +127,9 @@ const QuickMarcEditor = ({ const [isValidatedCurrentValues, setIsValidatedCurrentValues] = useState(false); const continueAfterSave = useRef(false); const formRef = useRef(null); + const lastFocusedInput = useRef(null); const confirmationChecks = useRef({ ...REQUIRED_CONFIRMATIONS }); - const { setValidationErrors } = useContext(QuickMarcContext); + const { setValidationErrors, setRelatedRecordVersion } = useContext(QuickMarcContext); const { hasErrorIssues, isBackEndValidationMarcType } = useValidation(); const isConsortiaEnv = stripes.hasInterface('consortia'); @@ -155,16 +152,6 @@ const QuickMarcEditor = ({ const saveFormDisabled = submitting || pristine; - const redirectToVersion = useCallback((updatedVersion) => { - const searchParams = new URLSearchParams(location.search); - - searchParams.set('relatedRecordVersion', updatedVersion); - - history.replace({ - search: searchParams.toString(), - }); - }, [history, location.search]); - const handleSubmitResponse = useCallback((updatedRecord) => { if (!updatedRecord?.version) { continueAfterSave.current = false; @@ -173,13 +160,14 @@ const QuickMarcEditor = ({ } if (continueAfterSave.current) { - redirectToVersion(updatedRecord.version); + setRelatedRecordVersion(updatedRecord.version); + lastFocusedInput.current?.focus(); return; } onSave(); - }, [redirectToVersion, onSave]); + }, [setRelatedRecordVersion, onSave]); const closeModals = () => { setIsDeleteModalOpened(false); @@ -462,6 +450,10 @@ const QuickMarcEditor = ({ } }, []); + const saveLastFocusedInput = useCallback((e) => { + lastFocusedInput.current = e.target; + }, [lastFocusedInput]); + const shortcuts = useMemo(() => ([{ name: 'save', shortcut: 'mod+s', @@ -568,6 +560,7 @@ const QuickMarcEditor = ({ linksCount={linksCount} isLoadingLinkSuggestions={isLoadingLinkSuggestions} onCheckCentralTenantPerm={onCheckCentralTenantPerm} + onInputFocus={saveLastFocusedInput} /> diff --git a/src/QuickMarcEditor/QuickMarcEditorContainer.js b/src/QuickMarcEditor/QuickMarcEditorContainer.js index f16030ea..bec87bc4 100644 --- a/src/QuickMarcEditor/QuickMarcEditorContainer.js +++ b/src/QuickMarcEditor/QuickMarcEditorContainer.js @@ -1,4 +1,4 @@ -import React, { +import { useEffect, useState, useCallback, @@ -9,10 +9,7 @@ import { withRouter } from 'react-router'; import ReactRouterPropTypes from 'react-router-prop-types'; import noop from 'lodash/noop'; -import { - stripesConnect, -} from '@folio/stripes/core'; - +import { stripesConnect } from '@folio/stripes/core'; import { LoadingView } from '@folio/stripes/components'; import { baseManifest, @@ -20,6 +17,8 @@ import { } from '@folio/stripes-acq-components'; import { getHeaders } from '@folio/stripes-marc-components'; +import { useAuthorityLinksCount } from '../queries'; +import { QuickMarcProvider } from '../contexts'; import { EXTERNAL_INSTANCE_APIS, MARC_RECORD_API, @@ -28,7 +27,6 @@ import { LINKING_RULES_API, MARC_SPEC_API, } from '../common/constants'; - import { dehydrateMarcRecordResponse, getCreateHoldingsMarcRecordResponse, @@ -40,8 +38,6 @@ import { applyCentralTenantInHeaders, } from './utils'; import { QUICK_MARC_ACTIONS } from './constants'; -import { useAuthorityLinksCount } from '../queries'; -import { QuickMarcProvider } from '../contexts'; const propTypes = { action: PropTypes.oneOf(Object.values(QUICK_MARC_ACTIONS)).isRequired, @@ -89,17 +85,15 @@ const QuickMarcEditorContainer = ({ const [locations, setLocations] = useState(); const [isLoading, setIsLoading] = useState(true); const [fixedFieldSpec, setFixedFieldSpec] = useState(); + const showCallout = useShowCallout(); + const { linksCount } = useAuthorityLinksCount({ id: marcType === MARC_TYPES.AUTHORITY && externalId }); - const searchParams = new URLSearchParams(location.search); const { token, locale } = stripes.okapi; const centralTenantId = stripes.user.user.consortium?.centralTenantId; const isRequestToCentralTenantFromMember = applyCentralTenantInHeaders(location, stripes, marcType) && action !== QUICK_MARC_ACTIONS.CREATE; - const showCallout = useShowCallout(); - const { linksCount } = useAuthorityLinksCount({ id: marcType === MARC_TYPES.AUTHORITY && externalId }); - const getCloseEditorParams = useCallback((id) => { if (marcType === MARC_TYPES.HOLDINGS && action !== QUICK_MARC_ACTIONS.CREATE) { return `${instanceId}/${externalId}`; @@ -125,8 +119,6 @@ const QuickMarcEditorContainer = ({ }, [externalRecordPath, marcType, externalId, instanceId, action]); const loadData = useCallback(async () => { - setIsLoading(true); - const path = action === QUICK_MARC_ACTIONS.CREATE && marcType === MARC_TYPES.HOLDINGS ? EXTERNAL_INSTANCE_APIS[MARC_TYPES.BIB] : EXTERNAL_INSTANCE_APIS[marcType]; @@ -175,14 +167,6 @@ const QuickMarcEditorContainer = ({ linkingRulesResponse, fixedFieldSpecResponse, ]) => { - if (action !== QUICK_MARC_ACTIONS.CREATE) { - searchParams.set('relatedRecordVersion', instanceResponse._version); - - history.replace({ - search: searchParams.toString(), - }); - } - let dehydratedMarcRecord; if (action === QUICK_MARC_ACTIONS.CREATE) { @@ -231,7 +215,7 @@ const QuickMarcEditorContainer = ({ } return ( - + { expect(getByText(instance.title)).toBeDefined(); }); - describe('when the action is not CREATE', () => { - it('should append the relatedRecordVersion parameter to URL', async () => { - const spyHistory = jest.spyOn(mockHistory, 'replace'); - - await act(async () => { - await renderQuickMarcEditorContainer({ - mutator, - onClose: jest.fn(), - action: QUICK_MARC_ACTIONS.EDIT, - wrapper: QuickMarcEditWrapper, - }); - }); - - expect(spyHistory).toHaveBeenCalledWith({ search: expect.stringContaining('relatedRecordVersion=1') }); - }); - }); - - describe('when the action is CREATE', () => { - it('should not append the relatedRecordVersion parameter to URL', async () => { - const history = createMemoryHistory(); - - history.replace = jest.fn(); - - await act(async () => { - await renderQuickMarcEditorContainer({ - mutator, - onClose: jest.fn(), - action: QUICK_MARC_ACTIONS.CREATE, - wrapper: QuickMarcEditWrapper, - history, - }); - }); - - expect(history.replace).not.toHaveBeenCalled(); - }); - }); - describe('Leader field', () => { describe('when the action is CREATE a bib record', () => { let recordLengthField; diff --git a/src/QuickMarcEditor/QuickMarcEditorRows/QuickMarcEditorRows.js b/src/QuickMarcEditor/QuickMarcEditorRows/QuickMarcEditorRows.js index 929b848f..293fb77e 100644 --- a/src/QuickMarcEditor/QuickMarcEditorRows/QuickMarcEditorRows.js +++ b/src/QuickMarcEditor/QuickMarcEditorRows/QuickMarcEditorRows.js @@ -96,6 +96,7 @@ const QuickMarcEditorRows = ({ linksCount, isLoadingLinkSuggestions, onCheckCentralTenantPerm, + onInputFocus, }) => { const location = useLocation(); const stripes = useStripes(); @@ -266,6 +267,7 @@ const QuickMarcEditorRows = ({ id="quick-marc-editor-rows" data-testid="quick-marc-editor-rows" ref={containerRef} + onFocus={onInputFocus} > ( }} subtype="m" isLoadingLinkSuggestions={false} + onInputFocus={jest.fn()} {...props} /> )} diff --git a/src/contexts/QuickMarcContext/QuickMarcContext.js b/src/contexts/QuickMarcContext/QuickMarcContext.js index 9df9a440..57106655 100644 --- a/src/contexts/QuickMarcContext/QuickMarcContext.js +++ b/src/contexts/QuickMarcContext/QuickMarcContext.js @@ -14,12 +14,15 @@ const propTypes = { PropTypes.arrayOf(PropTypes.node), PropTypes.node, ]).isRequired, + relatedRecordVersion: PropTypes.number, }; const QuickMarcProvider = ({ children, + relatedRecordVersion, }) => { const [selectedSourceFile, setSelectedSourceFile] = useState(null); + const [_relatedRecordVersion, setRelatedRecordVersion] = useState(relatedRecordVersion); const validationErrors = useRef({}); const setValidationErrors = useCallback((newValidationErrors) => { @@ -31,11 +34,15 @@ const QuickMarcProvider = ({ setSelectedSourceFile, validationErrorsRef: validationErrors, setValidationErrors, + relatedRecordVersion: _relatedRecordVersion, + setRelatedRecordVersion, }), [ selectedSourceFile, setSelectedSourceFile, validationErrors, setValidationErrors, + _relatedRecordVersion, + setRelatedRecordVersion, ]); return ( diff --git a/src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.js b/src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.js index 4b83b1e5..deb484e0 100644 --- a/src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.js +++ b/src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.js @@ -21,6 +21,8 @@ export const useFocusFirstFieldWithError = () => { return; } - document.querySelector(`[data-fieldid="${firstFieldWithErrors.id}"] input:enabled`)?.focus(); + const fieldSelector = `[data-fieldid="${firstFieldWithErrors.id}"]`; + + document.querySelector(`${fieldSelector} input:enabled, ${fieldSelector} textarea:enabled`)?.focus(); }, [firstFieldWithErrors?.id, validationErrorsRef]); }; diff --git a/test/jest/helpers/harness.js b/test/jest/helpers/harness.js index 9ad48cd9..abf84ce4 100644 --- a/test/jest/helpers/harness.js +++ b/test/jest/helpers/harness.js @@ -20,8 +20,13 @@ const defaultHistory = createMemoryHistory(); const queryClient = new QueryClient(); +const defaultQuickMarcContextValue = { + validationErrorsRef: { current: {} }, + setValidationErrors: jest.fn(), +}; + const QuickMarcProviderMock = ({ ctxValue, children }) => ( - + {children} );