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
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