From 91aba02304cd0d2400704ca2cd8e3e47866bd25c Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Mon, 11 Nov 2024 17:13:42 +0100 Subject: [PATCH 1/5] UIQM-730 Create/Edit/Derive MARC record - Retain focus when MARC record validation rules error display. Show validation issues toasts. --- CHANGELOG.md | 4 ++ package.json | 2 +- src/QuickMarcEditor/QuickMarcEditor.js | 55 +++++++++++++++---- .../QuickMarcEditorRows.js | 7 +-- src/hooks/index.js | 1 - .../useFocusFirstFieldWithError/index.js | 1 - .../useFocusFirstFieldWithError.js | 28 ---------- .../useFocusFirstFieldWithError.test.js | 51 ----------------- translations/ui-quick-marc/en.json | 3 + 9 files changed, 54 insertions(+), 98 deletions(-) delete mode 100644 src/hooks/useFocusFirstFieldWithError/index.js delete mode 100644 src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.js delete mode 100644 src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.test.js diff --git a/CHANGELOG.md b/CHANGELOG.md index abe8543b..57f62d47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Change history for ui-quick-marc +## [9.1.0] (IN PROGRESS) + +* [UIQM-730](https://issues.folio.org/browse/UIQM-730) Create/Edit/Derive MARC record - Retain focus when MARC record validation rules error display. Show validation issues toasts. + ## [9.0.1] (IN PROGRESS) * [UIQM-725](https://issues.folio.org/browse/UIQM-725) Fix wrong error message while saving MARC Bib record with invalid LDR position values. diff --git a/package.json b/package.json index aa506097..2e54542c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@folio/quick-marc", - "version": "9.0.0", + "version": "9.1.0", "description": "Quick MARC editor", "main": "index.js", "repository": "", diff --git a/src/QuickMarcEditor/QuickMarcEditor.js b/src/QuickMarcEditor/QuickMarcEditor.js index 3f90b497..1e35f3bb 100644 --- a/src/QuickMarcEditor/QuickMarcEditor.js +++ b/src/QuickMarcEditor/QuickMarcEditor.js @@ -136,6 +136,14 @@ const QuickMarcEditor = ({ const searchParameters = new URLSearchParams(location.search); const isShared = searchParameters.get('shared') === 'true'; + const saveLastFocusedInput = useCallback((e) => { + lastFocusedInput.current = e.target; + }, [lastFocusedInput]); + + const focusLastFocusedInput = useCallback(() => { + lastFocusedInput.current?.focus(); + }, [lastFocusedInput]); + const { unlinkAuthority } = useAuthorityLinking({ marcType, action }); useEffect(() => { @@ -161,13 +169,13 @@ const QuickMarcEditor = ({ if (continueAfterSave.current) { setRelatedRecordVersion(updatedRecord.version); - lastFocusedInput.current?.focus(); + focusLastFocusedInput(); return; } onSave(); - }, [setRelatedRecordVersion, onSave]); + }, [setRelatedRecordVersion, onSave, focusLastFocusedInput]); const closeModals = () => { setIsDeleteModalOpened(false); @@ -217,7 +225,7 @@ const QuickMarcEditor = ({ }; }, [setIsValidationModalOpen, isBackEndValidationMarcType, marcType]); - const showValidationIssuesCallouts = useCallback((issues) => { + const showErrorsForMissingFields = useCallback((issues) => { issues.forEach((error) => { showCallout({ message: error.message, @@ -228,6 +236,32 @@ const QuickMarcEditor = ({ }); }, [showCallout]); + const showValidationIssuesToasts = useCallback((validationErrors) => { + const allIssuesArray = Object.values(validationErrors).flat(); + const failCount = allIssuesArray.filter(issue => issue.severity === SEVERITY.ERROR).length; + const warnCount = allIssuesArray.length - failCount; + + const values = { + warnCount, + failCount, + }; + let messageId = null; + + if (failCount && warnCount) { + messageId = 'ui-quick-marc.record.save.error.failAndWarn'; + } else if (failCount) { + messageId = 'ui-quick-marc.record.save.error.fail'; + } else { + messageId = 'ui-quick-marc.record.save.error.warn'; + } + + showCallout({ + messageId, + values, + type: failCount ? 'error' : 'warning', + }); + }, [showCallout]); + const confirmSubmit = useCallback(async (e, isKeepEditing = false) => { continueAfterSave.current = isKeepEditing; let skipValidation = false; @@ -259,9 +293,12 @@ const QuickMarcEditor = ({ return; } + const validationErrorsWithoutFieldId = newValidationErrors[MISSING_FIELD_ID] || []; - showValidationIssuesCallouts(validationErrorsWithoutFieldId); + showErrorsForMissingFields(validationErrorsWithoutFieldId); + showValidationIssuesToasts(newValidationErrors); + focusLastFocusedInput(); setIsValidatedCurrentValues(true); } else { setValidationErrors({}); @@ -283,13 +320,15 @@ const QuickMarcEditor = ({ getState, hasErrorIssues, setValidationErrors, - showValidationIssuesCallouts, + showErrorsForMissingFields, showCallout, validate, runConfirmationChecks, isValidatedCurrentValues, setIsValidatedCurrentValues, manageBackendValidationModal, + focusLastFocusedInput, + showValidationIssuesToasts, ]); const paneFooter = useMemo(() => { @@ -450,10 +489,6 @@ const QuickMarcEditor = ({ } }, []); - const saveLastFocusedInput = useCallback((e) => { - lastFocusedInput.current = e.target; - }, [lastFocusedInput]); - const shortcuts = useMemo(() => ([{ name: 'save', shortcut: 'mod+s', @@ -580,7 +615,7 @@ const QuickMarcEditor = ({ /> { confirmRemoveAuthorityLinking && ( - + { - const { values } = useFormState(); - const { validationErrorsRef } = useContext(QuickMarcContext); - - const firstFieldWithErrors = useMemo(() => { - return values.records.find(({ id }) => Boolean(validationErrorsRef.current[id])); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [values.records, validationErrorsRef.current]); - - useEffect(() => { - if (!firstFieldWithErrors?.id) { - return; - } - - const fieldSelector = `[data-fieldid="${firstFieldWithErrors.id}"]`; - - document.querySelector(`${fieldSelector} input:enabled, ${fieldSelector} textarea:enabled`)?.focus(); - }, [firstFieldWithErrors?.id, validationErrorsRef]); -}; diff --git a/src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.test.js b/src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.test.js deleted file mode 100644 index 50baad04..00000000 --- a/src/hooks/useFocusFirstFieldWithError/useFocusFirstFieldWithError.test.js +++ /dev/null @@ -1,51 +0,0 @@ -import { - renderHook, - screen, -} from '@folio/jest-config-stripes/testing-library/react'; -import '@folio/jest-config-stripes/testing-library/jest-dom'; - -import Harness from '../../../test/jest/helpers/harness'; - -import { useFocusFirstFieldWithError } from './useFocusFirstFieldWithError'; - -jest.mock('react-final-form', () => ({ - useFormState: jest.fn().mockReturnValue({ - values: { - records: [{ - id: 'id-without-error', - }, { - id: 'id-with-error', - }], - }, - }), -})); - -const Wrapper = ({ children }) => ( - -
-
- {children} -
-); - -const renderUseFirstFieldWithError = () => { - return renderHook(() => useFocusFirstFieldWithError(), { wrapper: Wrapper }); -}; - -describe('Given useFocusFirstFieldWithError', () => { - describe('when a field has an error', () => { - it('should focus that field', () => { - renderUseFirstFieldWithError(); - - expect(screen.getByRole('textbox', { name: 'input-2' })).toHaveFocus(); - }); - }); -}); diff --git a/translations/ui-quick-marc/en.json b/translations/ui-quick-marc/en.json index 4e60e839..968e39ec 100644 --- a/translations/ui-quick-marc/en.json +++ b/translations/ui-quick-marc/en.json @@ -760,6 +760,9 @@ "record.save.error.notFound": "This record has been deleted by another user. You can no longer edit this record, hit the cancel button to return to the previous page.", "record.save.error.generic": "Record not saved: Communication problem with server. Please try again.", "record.save.error.derive": "Instance cannot be updated. Deprecated instance version.", + "record.save.error.fail": "{failCount} Fail error(s) was found. You cannot save record with a Fail error. Please resolve error(s) and hit Save to re-validate your record.", + "record.save.error.warn": "{warnCount} Warn error(s) was found. Hit Save again to continue to save record with Warn errors. Please resolve error(s) and hit Save to re-validate your record.", + "record.save.error.failAndWarn": "{warnCount} Warn error(s) was found. {failCount} Fail error(s) was found. You cannot save record with a Fail error. Please resolve error(s) and hit Save to re-validate your record.", "record.save.updatingLinkedBibRecords": "This record has successfully saved and is in process. {count} linked bibliographic record(s) updates have begun.", "record.save.continue": "Save & keep editing", From 2c6bd0a4babf483c0c130a171ddabee399363b34 Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Tue, 12 Nov 2024 11:59:25 +0100 Subject: [PATCH 2/5] UIQM-730 added tests for validation errors notifications --- src/QuickMarcEditor/QuickMarcEditor.js | 2 +- src/QuickMarcEditor/QuickMarcEditor.test.js | 85 +++++++++++++++++++-- 2 files changed, 81 insertions(+), 6 deletions(-) diff --git a/src/QuickMarcEditor/QuickMarcEditor.js b/src/QuickMarcEditor/QuickMarcEditor.js index 1e35f3bb..b63b78bf 100644 --- a/src/QuickMarcEditor/QuickMarcEditor.js +++ b/src/QuickMarcEditor/QuickMarcEditor.js @@ -615,7 +615,7 @@ const QuickMarcEditor = ({ /> { confirmRemoveAuthorityLinking && ( - + { }); describe('when saving form with validation errors and deleted fields', () => { - beforeEach(() => { + beforeEach(async () => { mockValidate.mockClear().mockResolvedValue({ [MISSING_FIELD_ID]: [{ id: 'some error', severity: 'error', values: {} }] }); - }); - it('should show errors and not show confirmation modal', async () => { const { getAllByRole, getByText, - queryByText, getByTestId, } = renderQuickMarcEditor(); @@ -811,9 +808,11 @@ describe('Given QuickMarcEditor', () => { fireEvent.change(contentField, { target: { value: '' } }); fireEvent.click(deleteButtons[0]); await fireEvent.click(getByText('stripes-acq-components.FormFooter.save')); + }); + it('should show errors and not show confirmation modal', async () => { await waitFor(() => { - expect(queryByText('Confirmation modal')).toBeNull(); + expect(screen.queryByText('Confirmation modal')).toBeNull(); expect(mockShowCallout).toHaveBeenCalledWith({ messageId: 'some error', values: {}, @@ -821,6 +820,82 @@ describe('Given QuickMarcEditor', () => { }); }); }); + + it('should show a toast notification about validation error', async () => { + await waitFor(() => { + expect(mockShowCallout).toHaveBeenCalledWith({ + messageId: 'ui-quick-marc.record.save.error.fail', + values: { + failCount: 1, + warnCount: 0, + }, + type: 'error', + }); + }); + }); + }); + + describe('when saving form with validation warnings', () => { + beforeEach(async () => { + mockValidate.mockClear().mockResolvedValue({ [MISSING_FIELD_ID]: [{ id: 'some warning', severity: 'warn', values: {} }] }); + + const { + getByText, + getByTestId, + } = renderQuickMarcEditor(); + + const contentField = getByTestId('content-field-3'); + + fireEvent.change(contentField, { target: { value: '' } }); + await fireEvent.click(getByText('stripes-acq-components.FormFooter.save')); + }); + + it('should show a toast notification about validation warning', async () => { + await waitFor(() => { + expect(mockShowCallout).toHaveBeenCalledWith({ + messageId: 'ui-quick-marc.record.save.error.warn', + values: { + failCount: 0, + warnCount: 1, + }, + type: 'warning', + }); + }); + }); + }); + + describe('when saving form with validation warnings and errors', () => { + beforeEach(async () => { + mockValidate.mockClear().mockResolvedValue({ + [MISSING_FIELD_ID]: [ + { id: 'some warning', severity: 'warn', values: {} }, + { id: 'some error', severity: 'error', values: {} }, + ], + }); + + const { + getByText, + getByTestId, + } = renderQuickMarcEditor(); + + const contentField = getByTestId('content-field-3'); + + fireEvent.change(contentField, { target: { value: '' } }); + await fireEvent.click(getByText('stripes-acq-components.FormFooter.save')); + }); + + it('should show a toast notification about validation warning and error', async () => { + await waitFor(() => { + expect(mockShowCallout).toHaveBeenCalledWith({ + messageId: 'ui-quick-marc.record.save.error.failAndWarn', + values: { + failCount: 1, + warnCount: 1, + }, + type: 'error', + }); + }); + }); }); describe('when marc record is of type HOLDINGS', () => { From c2d2d400e5f0d49b199be62ab223517b759ecbef Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Fri, 15 Nov 2024 13:13:05 +0100 Subject: [PATCH 3/5] UIQM-730 change the fail and warn toast text --- src/QuickMarcEditor/QuickMarcEditor.js | 1 + translations/ui-quick-marc/en.json | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/QuickMarcEditor/QuickMarcEditor.js b/src/QuickMarcEditor/QuickMarcEditor.js index b63b78bf..af6c7240 100644 --- a/src/QuickMarcEditor/QuickMarcEditor.js +++ b/src/QuickMarcEditor/QuickMarcEditor.js @@ -244,6 +244,7 @@ const QuickMarcEditor = ({ const values = { warnCount, failCount, + breakingLine:
, }; let messageId = null; diff --git a/translations/ui-quick-marc/en.json b/translations/ui-quick-marc/en.json index 968e39ec..b4a7e617 100644 --- a/translations/ui-quick-marc/en.json +++ b/translations/ui-quick-marc/en.json @@ -760,9 +760,9 @@ "record.save.error.notFound": "This record has been deleted by another user. You can no longer edit this record, hit the cancel button to return to the previous page.", "record.save.error.generic": "Record not saved: Communication problem with server. Please try again.", "record.save.error.derive": "Instance cannot be updated. Deprecated instance version.", - "record.save.error.fail": "{failCount} Fail error(s) was found. You cannot save record with a Fail error. Please resolve error(s) and hit Save to re-validate your record.", - "record.save.error.warn": "{warnCount} Warn error(s) was found. Hit Save again to continue to save record with Warn errors. Please resolve error(s) and hit Save to re-validate your record.", - "record.save.error.failAndWarn": "{warnCount} Warn error(s) was found. {failCount} Fail error(s) was found. You cannot save record with a Fail error. Please resolve error(s) and hit Save to re-validate your record.", + "record.save.error.warn": "Please scroll to view the entire record. Resolve errors as needed and save to revalidate the record.{breakingLine}Warn errors: {warnCount}", + "record.save.error.fail": "Please scroll to view the entire record. Resolve errors as needed and save to revalidate the record.{breakingLine}Fail errors: {failCount}{breakingLine}Record cannot be saved with a fail error.", + "record.save.error.failAndWarn": "Please scroll to view the entire record. Resolve errors as needed and save to revalidate the record.{breakingLine}Warn errors: {warnCount}{breakingLine}Fail errors: {failCount}{breakingLine}Record cannot be saved with a fail error.", "record.save.updatingLinkedBibRecords": "This record has successfully saved and is in process. {count} linked bibliographic record(s) updates have begun.", "record.save.continue": "Save & keep editing", From cfbf8967ad797dfb529b4ff7c1e13e458350d866 Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Fri, 15 Nov 2024 13:27:28 +0100 Subject: [PATCH 4/5] UIQM-730 fix tests --- src/QuickMarcEditor/QuickMarcEditor.js | 2 +- src/QuickMarcEditor/QuickMarcEditor.test.js | 24 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/QuickMarcEditor/QuickMarcEditor.js b/src/QuickMarcEditor/QuickMarcEditor.js index 4bbdbe97..0e1a04d0 100644 --- a/src/QuickMarcEditor/QuickMarcEditor.js +++ b/src/QuickMarcEditor/QuickMarcEditor.js @@ -235,7 +235,7 @@ const QuickMarcEditor = ({ const values = { warnCount, failCount, - breakingLine:
, + breakingLine:
, }; let messageId = null; diff --git a/src/QuickMarcEditor/QuickMarcEditor.test.js b/src/QuickMarcEditor/QuickMarcEditor.test.js index aa531d15..edc89a62 100644 --- a/src/QuickMarcEditor/QuickMarcEditor.test.js +++ b/src/QuickMarcEditor/QuickMarcEditor.test.js @@ -820,14 +820,14 @@ describe('Given QuickMarcEditor', () => { it('should show a toast notification about validation error', async () => { await waitFor(() => { - expect(mockShowCallout).toHaveBeenCalledWith({ + expect(mockShowCallout).toHaveBeenCalledWith(expect.objectContaining({ messageId: 'ui-quick-marc.record.save.error.fail', - values: { + values: expect.objectContaining({ failCount: 1, warnCount: 0, - }, + }), type: 'error', - }); + })); }); }); }); @@ -849,14 +849,14 @@ describe('Given QuickMarcEditor', () => { it('should show a toast notification about validation warning', async () => { await waitFor(() => { - expect(mockShowCallout).toHaveBeenCalledWith({ + expect(mockShowCallout).toHaveBeenCalledWith(expect.objectContaining({ messageId: 'ui-quick-marc.record.save.error.warn', - values: { + values: expect.objectContaining({ failCount: 0, warnCount: 1, - }, + }), type: 'warning', - }); + })); }); }); }); @@ -883,14 +883,14 @@ describe('Given QuickMarcEditor', () => { it('should show a toast notification about validation warning and error', async () => { await waitFor(() => { - expect(mockShowCallout).toHaveBeenCalledWith({ + expect(mockShowCallout).toHaveBeenCalledWith(expect.objectContaining({ messageId: 'ui-quick-marc.record.save.error.failAndWarn', - values: { + values: expect.objectContaining({ failCount: 1, warnCount: 1, - }, + }), type: 'error', - }); + })); }); }); }); From 59c448833d724228dbc87919efa1b2db0858c9b7 Mon Sep 17 00:00:00 2001 From: Denys Bohdan Date: Mon, 25 Nov 2024 11:10:48 +0100 Subject: [PATCH 5/5] UIQM-730 update error toast messages --- translations/ui-quick-marc/en.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/translations/ui-quick-marc/en.json b/translations/ui-quick-marc/en.json index b4a7e617..384ea316 100644 --- a/translations/ui-quick-marc/en.json +++ b/translations/ui-quick-marc/en.json @@ -760,9 +760,9 @@ "record.save.error.notFound": "This record has been deleted by another user. You can no longer edit this record, hit the cancel button to return to the previous page.", "record.save.error.generic": "Record not saved: Communication problem with server. Please try again.", "record.save.error.derive": "Instance cannot be updated. Deprecated instance version.", - "record.save.error.warn": "Please scroll to view the entire record. Resolve errors as needed and save to revalidate the record.{breakingLine}Warn errors: {warnCount}", - "record.save.error.fail": "Please scroll to view the entire record. Resolve errors as needed and save to revalidate the record.{breakingLine}Fail errors: {failCount}{breakingLine}Record cannot be saved with a fail error.", - "record.save.error.failAndWarn": "Please scroll to view the entire record. Resolve errors as needed and save to revalidate the record.{breakingLine}Warn errors: {warnCount}{breakingLine}Fail errors: {failCount}{breakingLine}Record cannot be saved with a fail error.", + "record.save.error.warn": "Please scroll to view the entire record. Resolve ussyes as needed and save to revalidate the record.{breakingLine}Warn errors: {warnCount}", + "record.save.error.fail": "Please scroll to view the entire record. Resolve issues as needed and save to revalidate the record.{breakingLine}Fail errors: {failCount}{breakingLine}Record cannot be saved with a fail error.", + "record.save.error.failAndWarn": "Please scroll to view the entire record. Resolve issues as needed and save to revalidate the record.{breakingLine}Warn errors: {warnCount}{breakingLine}Fail errors: {failCount}{breakingLine}Record cannot be saved with a fail error.", "record.save.updatingLinkedBibRecords": "This record has successfully saved and is in process. {count} linked bibliographic record(s) updates have begun.", "record.save.continue": "Save & keep editing",