Skip to content

Commit

Permalink
UIQM-730 Create/Edit/Derive MARC record - Retain focus when MARC reco…
Browse files Browse the repository at this point in the history
…rd validation rules error display. Show validation issues toasts. (#755)

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

* UIQM-730 added tests for validation errors notifications

* UIQM-730 change the fail and warn toast text

* UIQM-730 fix tests

* UIQM-730 update error toast messages
  • Loading branch information
BogdanDenis authored Nov 25, 2024
1 parent 9c8f180 commit 511e30c
Show file tree
Hide file tree
Showing 9 changed files with 130 additions and 101 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## [10.0.0] (IN PROGRESS)

* [UIQM-716](https://issues.folio.org/browse/UIQM-716) *BREAKING* Consolidate routes based on MARC type for bib and authority records to avoid page refresh after redirecting from the create page to the edit one.
* [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](https://github.com/folio-org/ui-quick-marc/tree/v9.0.1) (2024-11-22)

Expand Down
54 changes: 45 additions & 9 deletions src/QuickMarcEditor/QuickMarcEditor.js
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,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 @@ -156,9 +164,9 @@ const QuickMarcEditor = ({
const handleSubmitResponse = useCallback(() => {
if (continueAfterSave.current) {
continueAfterSave.current = false;
lastFocusedInput.current?.focus();
focusLastFocusedInput();
}
}, [continueAfterSave]);
}, [continueAfterSave, focusLastFocusedInput]);

const closeModals = () => {
setIsDeleteModalOpened(false);
Expand Down Expand Up @@ -208,7 +216,7 @@ const QuickMarcEditor = ({
};
}, [setIsValidationModalOpen, isBackEndValidationMarcType, marcType]);

const showValidationIssuesCallouts = useCallback((issues) => {
const showErrorsForMissingFields = useCallback((issues) => {
issues.forEach((error) => {
showCallout({
message: error.message,
Expand All @@ -219,6 +227,33 @@ 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,
breakingLine: <br />,
};
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 @@ -252,9 +287,12 @@ const QuickMarcEditor = ({

return;
}

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

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

Expand Down Expand Up @@ -444,10 +484,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 @@ -790,15 +790,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 @@ -808,16 +805,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(expect.objectContaining({
messageId: 'ui-quick-marc.record.save.error.fail',
values: expect.objectContaining({
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(expect.objectContaining({
messageId: 'ui-quick-marc.record.save.error.warn',
values: expect.objectContaining({
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(expect.objectContaining({
messageId: 'ui-quick-marc.record.save.error.failAndWarn',
values: expect.objectContaining({
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.warn": "<b>Please scroll to view the entire record. Resolve ussyes as needed and save to revalidate the record.</b>{breakingLine}Warn errors: <b>{warnCount}</b>",
"record.save.error.fail": "<b>Please scroll to view the entire record. Resolve issues as needed and save to revalidate the record.</b>{breakingLine}Fail errors: <b>{failCount}</b>{breakingLine}Record cannot be saved with a fail error.",
"record.save.error.failAndWarn": "<b>Please scroll to view the entire record. Resolve issues as needed and save to revalidate the record.</b>{breakingLine}Warn errors: <b>{warnCount}</b>{breakingLine}Fail errors: <b>{failCount}</b>{breakingLine}Record cannot be saved with a fail error.",
"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

0 comments on commit 511e30c

Please sign in to comment.