Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UIQM-730 Create/Edit/Derive MARC record - Retain focus when MARC record validation rules error display. Show validation issues toasts. #755

Merged
merged 7 commits into from
Nov 25, 2024
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@folio/quick-marc",
"version": "9.0.0",
"version": "9.1.0",
"description": "Quick MARC editor",
"main": "index.js",
"repository": "",
Expand Down
53 changes: 44 additions & 9 deletions src/QuickMarcEditor/QuickMarcEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand All @@ -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);
Expand Down Expand Up @@ -217,7 +225,7 @@ const QuickMarcEditor = ({
};
}, [setIsValidationModalOpen, isBackEndValidationMarcType, marcType]);

const showValidationIssuesCallouts = useCallback((issues) => {
const showErrorsForMissingFields = useCallback((issues) => {
issues.forEach((error) => {
showCallout({
message: error.message,
Expand All @@ -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;
Expand Down Expand Up @@ -259,9 +293,12 @@ const QuickMarcEditor = ({

return;
}

const validationErrorsWithoutFieldId = newValidationErrors[MISSING_FIELD_ID] || [];

showValidationIssuesCallouts(validationErrorsWithoutFieldId);
showErrorsForMissingFields(validationErrorsWithoutFieldId);
showValidationIssuesToasts(newValidationErrors);
focusLastFocusedInput();
setIsValidatedCurrentValues(true);
} else {
setValidationErrors({});
Expand All @@ -283,13 +320,15 @@ const QuickMarcEditor = ({
getState,
hasErrorIssues,
setValidationErrors,
showValidationIssuesCallouts,
showErrorsForMissingFields,
showCallout,
validate,
runConfirmationChecks,
isValidatedCurrentValues,
setIsValidatedCurrentValues,
manageBackendValidationModal,
focusLastFocusedInput,
showValidationIssuesToasts,
]);

const paneFooter = useMemo(() => {
Expand Down Expand Up @@ -450,10 +489,6 @@ const QuickMarcEditor = ({
}
}, []);

const saveLastFocusedInput = useCallback((e) => {
lastFocusedInput.current = e.target;
}, [lastFocusedInput]);

const shortcuts = useMemo(() => ([{
name: 'save',
shortcut: 'mod+s',
Expand Down
85 changes: 80 additions & 5 deletions src/QuickMarcEditor/QuickMarcEditor.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -793,15 +793,12 @@ describe('Given QuickMarcEditor', () => {
});

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();

Expand All @@ -811,16 +808,94 @@ 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: {},
type: 'error',
});
});
});

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', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ import {
isControlNumberRow,
isLeaderRow,
} from '../utils';
import {
useAuthorityLinking,
useFocusFirstFieldWithError,
} from '../../hooks';
import { useAuthorityLinking } from '../../hooks';
import { QuickMarcContext } from '../../contexts';
import {
QUICK_MARC_ACTIONS,
Expand Down Expand Up @@ -109,8 +106,6 @@ const QuickMarcEditorRows = ({
const childCalloutRef = useRef(null);
const { validationErrorsRef } = useContext(QuickMarcContext);

useFocusFirstFieldWithError();

const {
linkAuthority,
unlinkAuthority,
Expand Down
1 change: 0 additions & 1 deletion src/hooks/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export * from './useAuthorityLinking';
export * from './useSubfieldNavigation';
export * from './useValidation';
export * from './useFocusFirstFieldWithError';
1 change: 0 additions & 1 deletion src/hooks/useFocusFirstFieldWithError/index.js

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 3 additions & 0 deletions translations/ui-quick-marc/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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. <b>{count}</b> linked bibliographic record(s) updates have begun.",
"record.save.continue": "Save & keep editing",

Expand Down