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); + }); +});