diff --git a/CHANGELOG.md b/CHANGELOG.md index fd365c9e..0f01046f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * [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. +* [UIBULKED-570](https://folio-org.atlassian.net/browse/UIBULKED-570) Downloading files from Logs tab ## [4.2.2](https://github.com/folio-org/ui-bulk-edit/tree/v4.2.2) (2024-11-15) diff --git a/src/components/BulkEditActionMenu/BulkEditActionMenu.test.js b/src/components/BulkEditActionMenu/BulkEditActionMenu.test.js index 48dd3782..7b57cfb5 100644 --- a/src/components/BulkEditActionMenu/BulkEditActionMenu.test.js +++ b/src/components/BulkEditActionMenu/BulkEditActionMenu.test.js @@ -8,6 +8,7 @@ import { useOkapiKy } from '@folio/stripes/core'; import '../../../test/jest/__mock__'; import { runAxeTest } from '@folio/stripes-testing'; +import { omit } from 'lodash'; import { bulkEditLogsData } from '../../../test/jest/__mock__/fakeData'; import { queryClient } from '../../../test/jest/utils/queryClient'; @@ -19,8 +20,8 @@ import { CRITERIA, EDITING_STEPS, JOB_STATUSES, - FILE_KEYS, FILE_SEARCH_PARAMS, + LINK_KEYS, } from '../../constants'; import { useBulkOperationDetails } from '../../hooks/api'; @@ -36,10 +37,7 @@ const onToggle = jest.fn(); const bulkOperation = { ...bulkEditLogsData[0], status: JOB_STATUSES.DATA_MODIFICATION, - [FILE_KEYS.MATCHING_RECORDS_LINK]: FILE_KEYS.MATCHING_RECORDS_LINK, - [FILE_KEYS.UPDATED_RECORDS_LINK]: FILE_KEYS.UPDATED_RECORDS_LINK, - [FILE_KEYS.MATCHING_ERRORS_LINK]: FILE_KEYS.MATCHING_ERRORS_LINK, - [FILE_KEYS.UPDATED_ERRORS_LINK]: FILE_KEYS.UPDATED_ERRORS_LINK, + ...omit(LINK_KEYS, ['expired']), }; const defaultProviderState = { visibleColumns: [ diff --git a/src/components/BulkEditLogs/BulkEditLogsActions/BulkEditLogsActions.js b/src/components/BulkEditLogs/BulkEditLogsActions/BulkEditLogsActions.js index 3da8acff..5d5bc2ff 100644 --- a/src/components/BulkEditLogs/BulkEditLogsActions/BulkEditLogsActions.js +++ b/src/components/BulkEditLogs/BulkEditLogsActions/BulkEditLogsActions.js @@ -4,7 +4,6 @@ import React, { useEffect, } from 'react'; import PropTypes from 'prop-types'; -import { saveAs } from 'file-saver'; import { IconButton, DropdownMenu, @@ -16,12 +15,12 @@ import { } from '@folio/stripes/components'; import { FormattedMessage } from 'react-intl'; import { QUERY_KEY_DOWNLOAD_LOGS, useFileDownload } from '../../../hooks/api'; -import { APPROACHES, CAPABILITIES, linkNamesMap } from '../../../constants'; +import { APPROACHES, CAPABILITIES, LINK_KEYS } from '../../../constants'; import { useBulkPermissions } from '../../../hooks'; -import { getFileName } from '../../../utils/files'; +import { savePreviewFile } from '../../../utils/files'; const BulkEditLogsActions = ({ item }) => { - const fileNamePostfix = item.fqlQueryId ? `.${APPROACHES.QUERY}` : ''; + const fileNamePostfix = item?.fqlQueryId ? `.${APPROACHES.QUERY}` : ''; const { hasUsersViewPerms, @@ -33,9 +32,13 @@ const BulkEditLogsActions = ({ item }) => { queryKey: QUERY_KEY_DOWNLOAD_LOGS, enabled: false, id: item.id, - fileContentType: linkNamesMap[triggeredFile], - onSuccess: data => { - saveAs(new Blob([data]), getFileName(item, triggeredFile)); + fileContentType: LINK_KEYS[triggeredFile], + onSuccess: fileData => { + savePreviewFile({ + fileName: item?.[triggeredFile], + fileData, + }); + setTriggeredFile(null); }, }); @@ -50,7 +53,7 @@ const BulkEditLogsActions = ({ item }) => { setTriggeredFile(file); }; - const availableFiles = Object.keys(linkNamesMap).filter(linkName => item[linkName]); + const availableFiles = Object.keys(LINK_KEYS).filter(linkName => item[linkName]); const renderTrigger = useCallback(({ triggerRef, onToggle, ariaProps, keyHandler }) => ( ({ ...jest.requireActual('../../../hooks'), useBulkPermissions: jest.fn(), + useFileDownload: jest.fn(), +})); + +jest.mock('../../../hooks/api', () => ({ + useFileDownload: jest.fn(), })); jest.mock('../../../hooks/useSearchParams', () => ({ @@ -57,6 +55,19 @@ const renderBulkEditLogsActions = ({ item = bulkOperation } = {}) => { }; describe('BulkEditLogsActions', () => { + const mockPermissions = { + hasUsersViewPerms: true, + hasInventoryInstanceViewPerms: true, + }; + const mockRefetch = jest.fn(); + + beforeEach(() => { + useBulkPermissions.mockReturnValue(mockPermissions); + useFileDownload.mockReturnValue({ + refetch: mockRefetch, + }); + }); + beforeEach(() => { useOkapiKy.mockClear().mockReturnValue({}); }); diff --git a/src/components/BulkEditPane/BulkEditPane.js b/src/components/BulkEditPane/BulkEditPane.js index c4d48bd3..adddabbf 100644 --- a/src/components/BulkEditPane/BulkEditPane.js +++ b/src/components/BulkEditPane/BulkEditPane.js @@ -1,6 +1,5 @@ import React, { useCallback, useMemo, useState } from 'react'; import { FormattedMessage } from 'react-intl'; -import { saveAs } from 'file-saver'; import { Pane, @@ -22,7 +21,7 @@ import { CRITERIA, APPROACHES, FILE_SEARCH_PARAMS, - FILE_TO_LINK, + FILE_KEYS, } from '../../constants'; import { RootContext } from '../../context/RootContext'; import { BulkEditLogs } from '../BulkEditLogs/BulkEditLogs'; @@ -38,6 +37,7 @@ import { useResetFilters } from '../../hooks/useResetFilters'; import { BulkEditInAppLayer } from './BulkEditInAppLayer/BulkEditInAppLayer'; import { BulkEditMarcLayer } from './BulkEditMarcLayer/BulkEditMarcLayer'; +import { savePreviewFile } from '../../utils/files'; export const BulkEditPane = () => { const [isFileUploaded, setIsFileUploaded] = useState(false); @@ -103,10 +103,12 @@ export const BulkEditPane = () => { queryKey: QUERY_KEY_DOWNLOAD_ACTION_MENU, enabled: !!fileInfo, id: bulkOperationId, - fileContentType: FILE_SEARCH_PARAMS[fileInfo?.param]?.replace('_MARC', ''), - onSuccess: data => { - /* istanbul ignore next */ - saveAs(new Blob([data]), fileInfo?.bulkDetails[FILE_TO_LINK[fileInfo?.param]].split('/')[1]); + fileContentType: FILE_SEARCH_PARAMS[fileInfo?.param], + onSuccess: fileData => { + savePreviewFile({ + fileData, + fileName: fileInfo?.bulkDetails[FILE_KEYS[fileInfo?.param]], + }); }, onSettled: () => { /* istanbul ignore next */ diff --git a/src/constants/files.js b/src/constants/files.js index 33e8ca97..65d3b35e 100644 --- a/src/constants/files.js +++ b/src/constants/files.js @@ -2,65 +2,69 @@ import { FormattedMessage } from 'react-intl'; import React from 'react'; import { EDITING_STEPS } from './core'; -// use as marks that getFileName are ready -export const FILE_KEYS = { - MATCHING_RECORDS_LINK: 'linkToMatchedRecordsCsvFile', - MATCHING_ERRORS_LINK: 'linkToMatchedRecordsErrorsCsvFile', - PROPOSED_CHANGES_LINK: 'linkToModifiedRecordsCsvFile', - PROPOSED_CHANGES_LINK_MARC: 'linkToModifiedRecordsMarcFile', - UPDATE_CHANGES_LINK_MARC: 'linkToCommittedRecordsMarcFile', - UPDATED_RECORDS_LINK: 'linkToCommittedRecordsCsvFile', - UPDATED_ERRORS_LINK: 'linkToCommittedRecordsErrorsCsvFile', - TRIGGERING_FILE: 'linkToTriggeringCsvFile', -}; - // use as API key for /download export const FILE_SEARCH_PARAMS = { + TRIGGERING_FILE: 'TRIGGERING_FILE', MATCHED_RECORDS_FILE: 'MATCHED_RECORDS_FILE', RECORD_MATCHING_ERROR_FILE: 'RECORD_MATCHING_ERROR_FILE', COMMITTED_RECORDS_FILE: 'COMMITTED_RECORDS_FILE', + COMMITTED_RECORDS_MARC_FILE: 'COMMITTED_RECORDS_MARC_FILE', COMMITTING_CHANGES_ERROR_FILE: 'COMMITTING_CHANGES_ERROR_FILE', PROPOSED_CHANGES_FILE: 'PROPOSED_CHANGES_FILE', PROPOSED_CHANGES_MARC_FILE: 'PROPOSED_CHANGES_MARC_FILE', - COMMITTED_RECORDS_FILE_MARC: 'COMMITTED_RECORDS_FILE_MARC', }; -export const FILE_TO_LINK = { +// use as marks that getFileName are ready +export const FILE_KEYS = { + TRIGGERING_FILE: 'linkToTriggeringCsvFile', MATCHED_RECORDS_FILE: 'linkToMatchedRecordsCsvFile', RECORD_MATCHING_ERROR_FILE: 'linkToMatchedRecordsErrorsCsvFile', COMMITTED_RECORDS_FILE: 'linkToCommittedRecordsCsvFile', + COMMITTED_RECORDS_MARC_FILE: 'linkToCommittedRecordsMarcFile', COMMITTING_CHANGES_ERROR_FILE: 'linkToCommittedRecordsErrorsCsvFile', PROPOSED_CHANGES_FILE: 'linkToModifiedRecordsCsvFile', - COMMITTED_RECORDS_FILE_MARC: 'linkToCommittedRecordsMarcFile' + PROPOSED_CHANGES_MARC_FILE: 'linkToModifiedRecordsMarcFile', +}; + +export const LINK_KEYS = { + linkToTriggeringCsvFile: FILE_SEARCH_PARAMS.TRIGGERING_FILE, + linkToMatchedRecordsCsvFile: FILE_SEARCH_PARAMS.MATCHED_RECORDS_FILE, + linkToMatchedRecordsErrorsCsvFile: FILE_SEARCH_PARAMS.RECORD_MATCHING_ERROR_FILE, + linkToModifiedRecordsCsvFile: FILE_SEARCH_PARAMS.PROPOSED_CHANGES_FILE, + linkToModifiedRecordsMarcFile: FILE_SEARCH_PARAMS.PROPOSED_CHANGES_MARC_FILE, + linkToCommittedRecordsCsvFile: FILE_SEARCH_PARAMS.COMMITTED_RECORDS_FILE, + linkToCommittedRecordsMarcFile: FILE_SEARCH_PARAMS.COMMITTED_RECORDS_MARC_FILE, + linkToCommittedRecordsErrorsCsvFile: FILE_SEARCH_PARAMS.COMMITTING_CHANGES_ERROR_FILE, + expired: 'expired', }; export const getDownloadLinks = ({ perms, step }) => [ { - KEY: FILE_KEYS.MATCHING_RECORDS_LINK, + KEY: FILE_KEYS.MATCHED_RECORDS_FILE, SEARCH_PARAM: FILE_SEARCH_PARAMS.MATCHED_RECORDS_FILE, - LINK_NAME: , + LINK_NAME: , IS_VISIBLE: perms.hasAnyEditPermissions && step === EDITING_STEPS.UPLOAD, }, { - KEY: FILE_KEYS.UPDATED_RECORDS_LINK, + KEY: FILE_KEYS.COMMITTED_RECORDS_FILE, SEARCH_PARAM: FILE_SEARCH_PARAMS.COMMITTED_RECORDS_FILE, LINK_NAME: , IS_VISIBLE: perms.hasAnyEditPermissions, }, { - KEY: FILE_KEYS.UPDATE_CHANGES_LINK_MARC, - SEARCH_PARAM: FILE_SEARCH_PARAMS.COMMITTED_RECORDS_FILE_MARC, + KEY: FILE_KEYS.COMMITTED_RECORDS_MARC_FILE, + SEARCH_PARAM: FILE_SEARCH_PARAMS.COMMITTED_RECORDS_MARC_FILE, LINK_NAME: , IS_VISIBLE: perms.hasAnyEditPermissions, }, { - KEY: FILE_KEYS.MATCHING_ERRORS_LINK, + KEY: FILE_KEYS.RECORD_MATCHING_ERROR_FILE, SEARCH_PARAM: FILE_SEARCH_PARAMS.RECORD_MATCHING_ERROR_FILE, LINK_NAME: , IS_VISIBLE: perms.hasAnyEditPermissions && step === EDITING_STEPS.UPLOAD, }, { - KEY: FILE_KEYS.UPDATED_ERRORS_LINK, + KEY: FILE_KEYS.COMMITTING_CHANGES_ERROR_FILE, SEARCH_PARAM: FILE_SEARCH_PARAMS.COMMITTING_CHANGES_ERROR_FILE, LINK_NAME: , IS_VISIBLE: perms.hasAnyEditPermissions && step === EDITING_STEPS.COMMIT, diff --git a/src/constants/index.js b/src/constants/index.js index 7fb42390..5f991a1a 100644 --- a/src/constants/index.js +++ b/src/constants/index.js @@ -4,5 +4,4 @@ export * from './columns'; export * from './inAppActions'; export * from '../utils/date'; export * from './files'; -export * from './logsActions'; export * from './moduleNames'; diff --git a/src/constants/logsActions.js b/src/constants/logsActions.js deleted file mode 100644 index 469d47fb..00000000 --- a/src/constants/logsActions.js +++ /dev/null @@ -1,11 +0,0 @@ -export const linkNamesMap = { - linkToTriggeringCsvFile: 'TRIGGERING_FILE', - linkToMatchedRecordsCsvFile: 'MATCHED_RECORDS_FILE', - linkToMatchedRecordsErrorsCsvFile: 'RECORD_MATCHING_ERROR_FILE', - linkToModifiedRecordsCsvFile: 'PROPOSED_CHANGES_FILE', - linkToModifiedRecordsMarcFile: 'PROPOSED_CHANGES_FILE', - linkToCommittedRecordsMarcFile: 'COMMITTED_RECORDS_FILE', - linkToCommittedRecordsCsvFile: 'COMMITTED_RECORDS_FILE', - linkToCommittedRecordsErrorsCsvFile: 'COMMITTING_CHANGES_ERROR_FILE', - expired: 'expired', -}; diff --git a/src/utils/files.js b/src/utils/files.js index b7ef7ea9..5a38e7c5 100644 --- a/src/utils/files.js +++ b/src/utils/files.js @@ -1,25 +1,5 @@ import { saveAs } from 'file-saver'; -import { getFormattedFilePrefixDate } from './date'; - - -export const getFileName = (item, triggeredFile) => { - if (item.fqlQueryId) { - return { - linkToTriggeringCsvFile: `Query-${item.id}.csv`, - linkToMatchedRecordsCsvFile: `${getFormattedFilePrefixDate()}-Matched-Records-Query-${item.id}.csv`, - linkToModifiedRecordsCsvFile: `${getFormattedFilePrefixDate()}-Updates-Preview-Query-${item.id}.csv`, - linkToModifiedRecordsMarcFile: `${getFormattedFilePrefixDate()}-Updates-Preview-Query-${item.id}.mrc`, - linkToCommittedRecordsCsvFile: `${getFormattedFilePrefixDate()}-Changed-Records-Query-${item.id}.csv`, - linkToCommittedRecordsMarcFile: `${getFormattedFilePrefixDate()}-Changed-Records-Query-${item.id}.mrc`, - linkToCommittedRecordsErrorsCsvFile: `${getFormattedFilePrefixDate()}-Committing-changes-Errors-Query-${item.id}.csv`, - linkToMatchedRecordsErrorsCsvFile:`${getFormattedFilePrefixDate()}-Matching-Records-Errors-Query-${item.id}.csv`, - }[triggeredFile]; - } - - return item[triggeredFile].split('/')[1]; -}; - export const changeExtension = (fileName, extension) => { if (!fileName) return fileName; diff --git a/src/utils/files.test.js b/src/utils/files.test.js index 052a1c9c..094441fc 100644 --- a/src/utils/files.test.js +++ b/src/utils/files.test.js @@ -1,6 +1,6 @@ import { saveAs } from 'file-saver'; -import { getFileName, changeExtension, savePreviewFile } from './files'; +import { changeExtension, savePreviewFile } from './files'; import { getFormattedFilePrefixDate } from './date'; @@ -14,29 +14,6 @@ jest.mock('file-saver', () => ({ describe('files', () => { - describe('getFileName', () => { - it('should return the correct file name for Query approach - linkToTriggeringCsvFile', () => { - const item = { fqlQueryId: '111', id: 123 }; - const triggeredFile = 'linkToTriggeringCsvFile'; - const result = getFileName(item, triggeredFile); - expect(result).toBe('Query-123.csv'); - }); - - it('should return the correct file name for Query approach - linkToMatchedRecordsCsvFile', () => { - const item = { fqlQueryId: '111', id: 123 }; - const triggeredFile = 'linkToMatchedRecordsCsvFile'; - const result = getFileName(item, triggeredFile); - expect(result).toBe('mockedDate-Matched-Records-Query-123.csv'); - }); - - it('should return the correct file name for non-Query approach', () => { - const item = { fqlQueryId: null, linkToTriggeringCsvFile: 'somePath/someFile.csv' }; - const triggeredFile = 'linkToTriggeringCsvFile'; - const result = getFileName(item, triggeredFile); - expect(result).toBe('someFile.csv'); - }); - }); - describe('changeExtension', () => { it('should change the extension of a file', () => { expect(changeExtension('abc.csv', 'mrc')).toBe('abc.mrc'); diff --git a/src/utils/formatters.js b/src/utils/formatters.js index 0bb5b444..c975924e 100644 --- a/src/utils/formatters.js +++ b/src/utils/formatters.js @@ -5,9 +5,9 @@ import { NoValue } from '@folio/stripes/components'; import { FolioFormattedTime } from '@folio/stripes-acq-components'; import BulkEditLogsActions from '../components/BulkEditLogs/BulkEditLogsActions/BulkEditLogsActions'; -import { linkNamesMap } from '../constants'; +import { LINK_KEYS } from '../constants'; -const isActionsRendered = (item) => Object.keys(item).some(key => Object.keys(linkNamesMap).includes(key)); +const isActionsRendered = (item) => Object.keys(item).some(key => Object.keys(LINK_KEYS).includes(key)); export const getLogsResultsFormatter = () => ({ id: item => item.id, diff --git a/translations/ui-bulk-edit/en.json b/translations/ui-bulk-edit/en.json index 573eb275..46dda5b0 100644 --- a/translations/ui-bulk-edit/en.json +++ b/translations/ui-bulk-edit/en.json @@ -148,7 +148,7 @@ "textArea.resetAll": "Reset all", "start.downloadErrors": "Download errors (CSV)", - "start.downloadMathcedRecords": "Download matched records (CSV)", + "start.downloadMatchedRecords": "Download matched records (CSV)", "start.downloadChangedRecords": "Download changed records (CSV)", "start.downloadChangedRecords.marc": "Download changed records (MARC)", @@ -454,19 +454,19 @@ "logs.actions.linkToTriggeringCsvFile": "File that was used to trigger the bulk edit", "logs.actions.linkToMatchedRecordsCsvFile": "File with the matching records", "logs.actions.linkToMatchedRecordsErrorsCsvFile": "File with errors encountered during the record matching", - "logs.actions.linkToModifiedRecordsCsvFile": "File with the preview of proposed changes", - "logs.actions.linkToModifiedRecordsMarcFile": "File with the preview of proposed changes", - "logs.actions.linkToCommittedRecordsCsvFile": "File with updated records", - "logs.actions.linkToCommittedRecordsMarcFile": "File with updated records", + "logs.actions.linkToModifiedRecordsCsvFile": "File with the preview of proposed changes (CSV)", + "logs.actions.linkToModifiedRecordsMarcFile": "File with the preview of proposed changes (MARC)", + "logs.actions.linkToCommittedRecordsCsvFile": "File with updated records (CSV)", + "logs.actions.linkToCommittedRecordsMarcFile": "File with updated records (MARC)", "logs.actions.linkToCommittedRecordsErrorsCsvFile": "File with errors encountered when committing the changes", "logs.actions.linkToTriggeringCsvFile.QUERY": "File with identifiers of the records affected by bulk update", "logs.actions.linkToMatchedRecordsCsvFile.QUERY": "File with the matching records", "logs.actions.linkToMatchedRecordsErrorsCsvFile.QUERY": "File with errors encountered during the record matching", - "logs.actions.linkToModifiedRecordsCsvFile.QUERY": "File with the preview of proposed changes", - "logs.actions.linkToModifiedRecordsMarcFile.QUERY": "File with the preview of proposed changes", - "logs.actions.linkToCommittedRecordsCsvFile.QUERY": "File with updated records", + "logs.actions.linkToModifiedRecordsCsvFile.QUERY": "File with the preview of proposed changes (CSV)", + "logs.actions.linkToModifiedRecordsMarcFile.QUERY": "File with the preview of proposed changes (MARC)", + "logs.actions.linkToCommittedRecordsCsvFile.QUERY": "File with updated records (CSV)", "logs.actions.linkToCommittedRecordsErrorsCsvFile.QUERY": "File with errors encountered when committing the changes", - "logs.actions.linkToCommittedRecordsMarcFile.QUERY": "File with updated records", + "logs.actions.linkToCommittedRecordsMarcFile.QUERY": "File with updated records (MARC)", "logs.filter.title.status": "Statuses", "logs.filter.title.capability": "Record types", "logs.filter.title.types": "Bulk operation type",