diff --git a/CHANGELOG.md b/CHANGELOG.md
index 166c3c9c..fd365c9e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
* [UIBULKED-597](https://folio-org.atlassian.net/browse/UIBULKED-597) Commit changes button enabled before preview is populated
* [UIBULKED-599](https://folio-org.atlassian.net/browse/UIBULKED-599) Change Administrative note type is not supported for MARC instances.
* [UIBULKED-589](https://folio-org.atlassian.net/browse/UIBULKED-589) Make options in the "Actions" dropdown in "Bulk edits" in alphabetical order.
+* [UIBULKED-588](https://folio-org.atlassian.net/browse/IBULKED-588) Displaying errors and warnings.
## [4.2.2](https://github.com/folio-org/ui-bulk-edit/tree/v4.2.2) (2024-11-15)
diff --git a/src/components/BulkEditPane/BulkEditListResult/Preview/ErrorsAccordion/ErrorsAccordion.js b/src/components/BulkEditPane/BulkEditListResult/Preview/ErrorsAccordion/ErrorsAccordion.js
index 81373cbe..14b2f9b8 100644
--- a/src/components/BulkEditPane/BulkEditListResult/Preview/ErrorsAccordion/ErrorsAccordion.js
+++ b/src/components/BulkEditPane/BulkEditListResult/Preview/ErrorsAccordion/ErrorsAccordion.js
@@ -12,7 +12,7 @@ import { useStripes } from '@folio/stripes/core';
import { PrevNextPagination } from '@folio/stripes-acq-components';
import css from '../Preview.css';
import { useSearchParams } from '../../../../../hooks';
-import { CAPABILITIES, ERROR_PARAMETERS_KEYS } from '../../../../../constants';
+import { CAPABILITIES, ERROR_PARAMETERS_KEYS, ERROR_TYPES } from '../../../../../constants';
const getParam = (error, key) => error.parameters.find(param => param.key === key)?.value;
@@ -24,6 +24,14 @@ const columnMapping = {
const visibleColumns = Object.keys(columnMapping);
+const renderErrorType = (error) => {
+ if (!error.type || error.type === ERROR_TYPES.ERROR) {
+ return ;
+ }
+
+ return ;
+};
+
const renderErrorMessage = (error, isLinkAvailable) => {
const link = getParam(error, ERROR_PARAMETERS_KEYS.LINK);
@@ -45,16 +53,19 @@ const renderErrorMessage = (error, isLinkAvailable) => {
};
const getResultsFormatter = ({ isLinkAvailable }) => ({
- type: () => ,
+ type: renderErrorType,
key: error => getParam(error, ERROR_PARAMETERS_KEYS.IDENTIFIER),
message: error => renderErrorMessage(error, isLinkAvailable),
});
const ErrorsAccordion = ({
errors = [],
+ errorType,
totalErrors,
+ totalWarnings,
isFetching,
pagination,
+ onShowWarnings,
onChangePage,
}) => {
const { user, okapi } = useStripes();
@@ -65,13 +76,11 @@ const ErrorsAccordion = ({
const isLinkAvailable = (isCentralTenant && capabilities === CAPABILITIES.INSTANCE) || !isCentralTenant;
const resultsFormatter = getResultsFormatter({ isLinkAvailable });
const errorLength = errors.length;
+ // temporary solution to calculate total errors and warnings, until backend will provide it in scope of MODBULKOPS-451
+ const totalErrorsAndWarnings = errorType === ERROR_TYPES.ERROR ? totalErrors : totalErrors + totalWarnings;
+ const isWarningsCheckboxDisabled = !totalWarnings || !totalErrors;
const [opened, setOpened] = useState(!!errorLength);
- const [showWarnings, setShowWarnings] = useState(false);
-
- const handleShowWarnings = () => {
- setShowWarnings(prev => !prev);
- };
return (
@@ -89,13 +98,14 @@ const ErrorsAccordion = ({
id="ui-bulk-edit.list.errors.info"
values={{
errors: totalErrors,
- warnings: 0,
+ warnings: totalWarnings,
}}
/>
}
- checked={showWarnings}
- onChange={handleShowWarnings}
+ checked={!errorType}
+ onChange={onShowWarnings}
+ disabled={isWarningsCheckboxDisabled}
/>
@@ -113,7 +123,7 @@ const ErrorsAccordion = ({
{errors.length > 0 && (
@@ -128,11 +138,14 @@ const ErrorsAccordion = ({
ErrorsAccordion.propTypes = {
errors: PropTypes.arrayOf(PropTypes.object),
totalErrors: PropTypes.number,
+ totalWarnings: PropTypes.number,
+ errorType: PropTypes.string,
isFetching: PropTypes.bool,
pagination: {
limit: PropTypes.number,
offset: PropTypes.number,
},
+ onShowWarnings: PropTypes.func,
onChangePage: PropTypes.func,
};
diff --git a/src/components/BulkEditPane/BulkEditListResult/Preview/Preview.js b/src/components/BulkEditPane/BulkEditListResult/Preview/Preview.js
index a08f4626..287ed5ae 100644
--- a/src/components/BulkEditPane/BulkEditListResult/Preview/Preview.js
+++ b/src/components/BulkEditPane/BulkEditListResult/Preview/Preview.js
@@ -26,6 +26,7 @@ import { usePagination } from '../../../../hooks/usePagination';
import { useBulkOperationStats } from '../../../../hooks/useBulkOperationStats';
import { NoResultsMessage } from '../NoResultsMessage/NoResultsMessage';
import { useSearchParams } from '../../../../hooks';
+import { useErrorType } from '../../../../hooks/useErrorType';
export const Preview = ({ id, title, isInitial, bulkDetails }) => {
const {
@@ -43,9 +44,15 @@ export const Preview = ({ id, title, isInitial, bulkDetails }) => {
const {
countOfRecords,
countOfErrors,
+ countOfWarnings,
visibleColumns,
} = useBulkOperationStats({ bulkDetails, step });
+ const { errorType, toggleErrorType } = useErrorType({
+ countOfErrors,
+ countOfWarnings
+ });
+
const {
pagination: previewPagination,
changePage: changePreviewPage,
@@ -71,10 +78,17 @@ export const Preview = ({ id, title, isInitial, bulkDetails }) => {
const { errors, isFetching: isErrorsFetching } = useErrorsPreview({
id,
+ step,
+ errorType,
enabled: isPreviewEnabled,
...errorsPagination,
});
+ const handleToggleWarnings = () => {
+ changeErrorPage(ERRORS_PAGINATION_CONFIG);
+ toggleErrorType();
+ };
+
if (!((bulkDetails.fqlQuery && criteria === CRITERIA.QUERY) || (criteria !== CRITERIA.QUERY && !bulkDetails.fqlQuery))) {
return
;
}
@@ -117,7 +131,10 @@ export const Preview = ({ id, title, isInitial, bulkDetails }) => {
diff --git a/src/components/shared/ProgressBar/utils.js b/src/components/shared/ProgressBar/utils.js
index bd2da5c8..c6d579e0 100644
--- a/src/components/shared/ProgressBar/utils.js
+++ b/src/components/shared/ProgressBar/utils.js
@@ -10,7 +10,7 @@ export const getBulkOperationStep = (bulkOperation) => {
case bulkOperation.status === JOB_STATUSES.COMPLETED:
case (
bulkOperation.status === JOB_STATUSES.COMPLETED_WITH_ERRORS
- && Boolean(bulkOperation.committedNumOfErrors)
+ && (Boolean(bulkOperation.committedNumOfErrors) || Boolean(bulkOperation.committedNumOfWarnings))
):
return EDITING_STEPS.COMMIT;
case bulkOperation.status === JOB_STATUSES.DATA_MODIFICATION:
diff --git a/src/constants/core.js b/src/constants/core.js
index 22ce450e..e81b1787 100644
--- a/src/constants/core.js
+++ b/src/constants/core.js
@@ -7,6 +7,11 @@ export const PREVIEW_LIMITS = {
RECORDS: 100,
};
+export const ERROR_TYPES = {
+ WARNING: 'WARNING',
+ ERROR: 'ERROR',
+};
+
export const APPROACHES = {
IN_APP: 'IN_APP',
MANUAL: 'MANUAL',
@@ -151,7 +156,7 @@ export const ERROR_PARAMETERS_KEYS = {
};
export const RECORD_TYPES_MAPPING = {
- [CAPABILITIES.HOLDING]: 'holding',
+ [CAPABILITIES.HOLDING]: 'holdings',
[CAPABILITIES.INSTANCE]: 'instance',
[CAPABILITIES.ITEM]: 'item',
[CAPABILITIES.USER]: 'user',
diff --git a/src/hooks/api/useErrorsPreview.js b/src/hooks/api/useErrorsPreview.js
index cc91e749..9c2a6600 100644
--- a/src/hooks/api/useErrorsPreview.js
+++ b/src/hooks/api/useErrorsPreview.js
@@ -10,6 +10,8 @@ export const useErrorsPreview = ({
enabled,
offset = 0,
limit = PREVIEW_LIMITS.ERRORS,
+ step,
+ errorType
}) => {
const ky = useOkapiKy();
const [namespaceKey] = useNamespace({ key: PREVIEW_ERRORS_KEY });
@@ -18,8 +20,8 @@ export const useErrorsPreview = ({
const { data, isFetching } = useQuery(
{
- queryKey: [namespaceKey, id, limit, offset],
- queryFn: () => ky.get(`bulk-operations/${id}/errors`, { searchParams: { limit, offset } }).json(),
+ queryKey: [namespaceKey, id, limit, offset, errorType, step],
+ queryFn: () => ky.get(`bulk-operations/${id}/errors`, { searchParams: { limit, offset, errorType } }).json(),
onError: showErrorMessage,
onSuccess: showErrorMessage,
keepPreviousData: true,
diff --git a/src/hooks/api/useErrorsPreview.test.js b/src/hooks/api/useErrorsPreview.test.js
index 5bfd6b65..c66164e6 100644
--- a/src/hooks/api/useErrorsPreview.test.js
+++ b/src/hooks/api/useErrorsPreview.test.js
@@ -5,7 +5,7 @@ import { useNamespace, useOkapiKy } from '@folio/stripes/core';
import { useErrorMessages } from '../useErrorMessages';
import { useErrorsPreview, PREVIEW_ERRORS_KEY } from './useErrorsPreview';
-import { PREVIEW_LIMITS } from '../../constants';
+import { EDITING_STEPS, ERROR_TYPES, PREVIEW_LIMITS } from '../../constants';
jest.mock('react-query', () => ({
useQuery: jest.fn(),
@@ -40,7 +40,12 @@ describe('useErrorsPreview', () => {
isFetching: true,
});
- const { result } = renderHook(() => useErrorsPreview({ id: '123', enabled: true }));
+ const { result } = renderHook(() => useErrorsPreview({
+ id: '123',
+ enabled: true,
+ errorType: ERROR_TYPES.ERROR,
+ step: EDITING_STEPS.UPLOAD,
+ }));
expect(result.current.errors).toEqual(['error1', 'error2']);
expect(result.current.isFetching).toBe(true);
@@ -48,7 +53,7 @@ describe('useErrorsPreview', () => {
expect(useNamespace).toHaveBeenCalledWith({ key: PREVIEW_ERRORS_KEY });
expect(useQuery).toHaveBeenCalledWith(
expect.objectContaining({
- queryKey: [PREVIEW_ERRORS_KEY, '123', PREVIEW_LIMITS.ERRORS, 0],
+ queryKey: [PREVIEW_ERRORS_KEY, '123', PREVIEW_LIMITS.ERRORS, 0, ERROR_TYPES.ERROR, EDITING_STEPS.UPLOAD],
queryFn: expect.any(Function),
enabled: true,
})
@@ -62,10 +67,16 @@ describe('useErrorsPreview', () => {
return { data: null, isFetching: false };
});
- renderHook(() => useErrorsPreview({ id: '123', enabled: true, offset: 10, limit: 20 }));
+ renderHook(() => useErrorsPreview({
+ id: '123',
+ enabled: true,
+ offset: 10,
+ limit: 20,
+ errorType: ERROR_TYPES.WARNING
+ }));
expect(mockGet).toHaveBeenCalledWith('bulk-operations/123/errors', {
- searchParams: { limit: 20, offset: 10 },
+ searchParams: { limit: 20, offset: 10, errorType: ERROR_TYPES.WARNING },
});
});
diff --git a/src/hooks/useBulkOperationStats.js b/src/hooks/useBulkOperationStats.js
index fd965602..bd0bf8b2 100644
--- a/src/hooks/useBulkOperationStats.js
+++ b/src/hooks/useBulkOperationStats.js
@@ -5,6 +5,7 @@ import { RootContext } from '../context/RootContext';
export const useBulkOperationStats = ({ bulkDetails, step }) => {
const { countOfRecords, setCountOfRecords, visibleColumns } = useContext(RootContext);
const [countOfErrors, setCountOfErrors] = useState(0);
+ const [countOfWarnings, setCountOfWarnings] = useState(0);
const [totalCount, setTotalCount] = useState(0);
useEffect(() => {
@@ -18,7 +19,12 @@ export const useBulkOperationStats = ({ bulkDetails, step }) => {
? bulkDetails.matchedNumOfErrors
: bulkDetails.committedNumOfErrors;
+ const countWarnings = isInitialPreview
+ ? bulkDetails.matchedNumOfWarnings
+ : bulkDetails.committedNumOfWarnings;
+
setCountOfErrors(countErrors);
+ setCountOfWarnings(countWarnings);
setCountOfRecords(countRecords);
setTotalCount(isInitialPreview ? bulkDetails.totalNumOfRecords : bulkDetails.matchedNumOfRecords);
}, [
@@ -32,6 +38,7 @@ export const useBulkOperationStats = ({ bulkDetails, step }) => {
return {
countOfRecords,
countOfErrors,
+ countOfWarnings,
totalCount,
visibleColumns,
};
diff --git a/src/hooks/useErrorType.js b/src/hooks/useErrorType.js
new file mode 100644
index 00000000..79295399
--- /dev/null
+++ b/src/hooks/useErrorType.js
@@ -0,0 +1,22 @@
+import { useEffect, useState } from 'react';
+import { ERROR_TYPES } from '../constants';
+
+// empty string is used to reset the error type and show both errors and warnings
+const getDynamicErrorType = (condition) => (condition ? '' : ERROR_TYPES.ERROR);
+
+export const useErrorType = ({ countOfErrors, countOfWarnings }) => {
+ const hasOnlyWarnings = countOfErrors === 0 && countOfWarnings > 0;
+ const initialErrorType = getDynamicErrorType(hasOnlyWarnings);
+
+ const [errorType, setErrorType] = useState(initialErrorType);
+
+ const toggleErrorType = () => {
+ setErrorType(getDynamicErrorType(!!errorType));
+ };
+
+ useEffect(() => {
+ setErrorType(initialErrorType);
+ }, [initialErrorType]);
+
+ return { errorType, toggleErrorType };
+};
diff --git a/src/hooks/useErrorType.test.js b/src/hooks/useErrorType.test.js
new file mode 100644
index 00000000..f204765e
--- /dev/null
+++ b/src/hooks/useErrorType.test.js
@@ -0,0 +1,48 @@
+import { renderHook, act } from '@testing-library/react-hooks';
+import { useErrorType } from './useErrorType';
+import { ERROR_TYPES } from '../constants';
+
+describe('useErrorType', () => {
+ it('should initialize with no error type if only warnings exist', () => {
+ const { result } = renderHook(() => useErrorType({ countOfErrors: 0, countOfWarnings: 1 }));
+ expect(result.current.errorType).toBe('');
+ });
+
+ it('should initialize with error type if errors exist', () => {
+ const { result } = renderHook(() => useErrorType({ countOfErrors: 1, countOfWarnings: 0 }));
+ expect(result.current.errorType).toBe(ERROR_TYPES.ERROR);
+ });
+
+ it('should toggle error type when toggleErrorType is called', () => {
+ const { result } = renderHook(() => useErrorType({ countOfErrors: 1, countOfWarnings: 0 }));
+
+ expect(result.current.errorType).toBe(ERROR_TYPES.ERROR);
+
+ act(() => {
+ result.current.toggleErrorType();
+ });
+ expect(result.current.errorType).toBe('');
+
+ act(() => {
+ result.current.toggleErrorType();
+ });
+ expect(result.current.errorType).toBe(ERROR_TYPES.ERROR);
+ });
+
+ it('should reset error type when the input props change', () => {
+ const { result, rerender } = renderHook(
+ ({ countOfErrors, countOfWarnings }) => useErrorType({ countOfErrors, countOfWarnings }),
+ {
+ initialProps: { countOfErrors: 1, countOfWarnings: 0 },
+ }
+ );
+
+ expect(result.current.errorType).toBe(ERROR_TYPES.ERROR);
+
+ rerender({ countOfErrors: 0, countOfWarnings: 1 });
+ expect(result.current.errorType).toBe('');
+
+ rerender({ countOfErrors: 2, countOfWarnings: 0 });
+ expect(result.current.errorType).toBe(ERROR_TYPES.ERROR);
+ });
+});