diff --git a/src/js/actions/current.js b/src/js/actions/current.js index 2c75fc13..93d63411 100644 --- a/src/js/actions/current.js +++ b/src/js/actions/current.js @@ -1,13 +1,17 @@ import { omit } from 'web-common/utils'; import { getApiForItems, splitItemAndCollectionKeys } from '../common/actions'; -import { exportItems, chunkedToggleTagsOnItems, chunkedAddToCollection, chunkedCopyToLibrary, +import { + exportItems, chunkedToggleTagsOnItems, chunkedAddToCollection, chunkedCopyToLibrary, chunkedDeleteItems, chunkedMoveItemsToTrash, chunkedRecoverItemsFromTrash, chunkedRemoveFromCollection, chunkedUpdateCollectionsTrash, chunkedDeleteCollections, createItem, - createItemOfType, toggleModal, navigate, retrieveMetadata, undoRetrieveMetadata } from '.'; + createItemOfType, toggleModal, navigate, retrieveMetadata, undoRetrieveMetadata +} from '.'; import columnProperties from '../constants/column-properties'; import { BIBLIOGRAPHY, COLLECTION_SELECT, EXPORT, NEW_ITEM } from '../constants/modals'; import { TOGGLE_ADD, TOGGLE_REMOVE } from '../common/tags'; +import { BEGIN_ONGOING, COMPLETE_ONGOING, CLEAR_ONGOING } from '../constants/actions'; +import { getUniqueId } from '../utils'; const currentDuplicateItem = () => { return async (dispatch, getState) => { @@ -173,7 +177,7 @@ const currentToggleTagByIndex = (tagPosition) => { const { libraryKey, itemKeys } = state.current; const items = state.libraries[libraryKey].items; const tagColors = state.libraries[libraryKey].tagColors?.value; - if(!tagColors[tagPosition]) { + if (!tagColors[tagPosition]) { return; } const tagToToggle = tagColors[tagPosition].name; @@ -202,12 +206,12 @@ const currentGoToSubscribeUrl = () => { const sortAndDirection = { sort, direction }; var pretendedResponse; - switch(itemsSource) { + switch (itemsSource) { case 'query': pretendedResponse = await getApiForItems( { config, libraryKey }, 'ITEMS_BY_QUERY', { collectionKey, isTrash, isMyPublications } ).pretend('get', null, { q, tag, qmode, ...sortAndDirection, format: 'atom' }); - break; + break; case 'top': pretendedResponse = await getApiForItems( { config, libraryKey }, 'TOP_ITEMS', {} @@ -232,14 +236,14 @@ const currentGoToSubscribeUrl = () => { const redirectUrl = pretendedResponse.getData().url; - if(isPublic) { + if (isPublic) { window.open(redirectUrl); return; } const apiKeyBase = websiteUrl + 'settings/keys/new'; const qparams = { 'name': 'Private Feed' }; - if(isGroupLibrary){ + if (isGroupLibrary) { qparams['library_access'] = 0; qparams['group_' + libraryId] = 'read'; qparams['redirect'] = redirectUrl; @@ -263,8 +267,37 @@ const currentRetrieveMetadata = () => { const state = getState(); const { itemKeys: keys, libraryKey } = state.current; const { itemKeys } = splitItemAndCollectionKeys(keys, libraryKey, state); - const promises = itemKeys.map(key => dispatch(retrieveMetadata(key, libraryKey))); - return await Promise.all(promises); + const backgroundTasks = state.ongoing.filter(p => p.kind === 'metadata-retrieval') ?? []; + + // Reset any ongoing metadata retrieval background tasks. We will still account for any items that were already being retrieved. + backgroundTasks.forEach(task => { + dispatch({ id: task.id, type: CLEAR_ONGOING }); + }); + + + const id = getUniqueId(); + dispatch({ + id, + data: { + // count previously recognized items and new items, but only count each item once + count: new Set([...state.recognize.entries.map(e => e.itemKey), ...itemKeys]).size, + }, + kind: 'metadata-retrieval', + skipUI: true, // skip displaying the "ongoing". This will be toggled when modal is closed. + libraryKey, + type: BEGIN_ONGOING, + }); + const promises = itemKeys.map(key => dispatch(retrieveMetadata(key, libraryKey, id ))); + + Promise.all(promises) + .finally(() => { + dispatch({ + id, + kind: 'metadata-retrieval', + type: COMPLETE_ONGOING, + }); + }); + return promises; } } diff --git a/src/js/actions/recognize.js b/src/js/actions/recognize.js index 2677616a..1b1a0514 100644 --- a/src/js/actions/recognize.js +++ b/src/js/actions/recognize.js @@ -27,9 +27,9 @@ const getItemFromIdentifier = identifier => { } } -const retrieveMetadata = (itemKey, libraryKey) => { +const retrieveMetadata = (itemKey, libraryKey, backgroundTaskId) => { return async (dispatch, getState) => { - dispatch({ type: BEGIN_RECOGNIZE_DOCUMENT, itemKey, libraryKey }); + dispatch({ type: BEGIN_RECOGNIZE_DOCUMENT, itemKey, libraryKey, backgroundTaskId }); const state = getState(); const attachmentItem = state.libraries[state.current.libraryKey]?.items?.[itemKey]; try { diff --git a/src/js/component/modal/metadata-retrieval.jsx b/src/js/component/modal/metadata-retrieval.jsx index ce192f60..3bdeaac8 100644 --- a/src/js/component/modal/metadata-retrieval.jsx +++ b/src/js/component/modal/metadata-retrieval.jsx @@ -162,8 +162,10 @@ const MetadataRetrievalModal = () => { const dispatch = useDispatch(); const isTouchOrSmall = useSelector(state => state.device.isTouchOrSmall); const isOpen = useSelector(state => state.modal.id === METADATA_RETRIEVAL); + const recognizeSelected = useSelector(state => state.modal?.recognizeSelected); const wasOpen = usePrevious(isOpen); const recognizeProgress = useSelector(state => state.recognize.progress); + const backgroundTaskId = useSelector(state => state.recognize.backgroundTaskId); const recognizeEntries = useSelector(state => state.recognize.entries, shallowEqual); const isDone = recognizeProgress === 1; const columns = [ @@ -200,16 +202,29 @@ const MetadataRetrievalModal = () => { if(isTouchOrSmall && !isDone) { return; } + // if recognition is done, clear the modal and the recognition state when closing + if ((isDone || recognizeEntries.length === 0) && backgroundTaskId) { + dispatch({ type: 'CLEAR_ONGOING', id: backgroundTaskId }); + } dispatch(toggleModal()); - }, [dispatch, isDone, isTouchOrSmall]); + }, [backgroundTaskId, dispatch, isDone, isTouchOrSmall, recognizeEntries]); useEffect(() => { if (isOpen && !wasOpen) { - dispatch(currentRetrieveMetadata()); - // unselect items to be recognized. If recognition is successful, the items will become child items and thus disappear from the list - setTimeout(() => { dispatch(navigate({ items: [] })); }, 0); + if (backgroundTaskId) { + dispatch({ type: 'UPDATE_ONGOING', id: backgroundTaskId, skipUI: true }); + } + if (recognizeSelected) { + dispatch(currentRetrieveMetadata()); + // unselect items to be recognized. If recognition is successful, the items will become child items and thus disappear from the list + setTimeout(() => { dispatch(navigate({ items: [] })); }, 0); + } + } else if (!isOpen && wasOpen) { + if (backgroundTaskId) { + dispatch({ type: 'UPDATE_ONGOING', id: backgroundTaskId, skipUI: false }); + } } - }, [dispatch, isOpen, wasOpen]); + }, [backgroundTaskId, dispatch, isOpen, recognizeSelected, wasOpen]); const sharedProps = { columns, totalResults: recognizeEntries.length, itemCount: recognizeEntries.length, getItemData diff --git a/src/js/component/ongoing.jsx b/src/js/component/ongoing.jsx index c21041cd..314c9749 100644 --- a/src/js/component/ongoing.jsx +++ b/src/js/component/ongoing.jsx @@ -8,12 +8,19 @@ import { usePrevious } from 'web-common/hooks'; import Modal from './ui/modal'; import { maxByKey } from '../utils'; -import { navigate } from '../actions'; +import { navigate, toggleModal } from '../actions'; +import { METADATA_RETRIEVAL } from '../constants/modals'; + +const defaultAction = (process, dispatch) => { + dispatch({ type: 'CLEAR_ONGOING', id: process.id }); +}; + const PROCESSES = { 'upload': { title: 'File Upload', action: (process, dispatch) => { + dispatch({ type: 'CLEAR_ONGOING', id: process.id }); dispatch(navigate({ library: process.data.libraryKey, collection: process.data?.collectionKey, @@ -28,30 +35,38 @@ const PROCESSES = { title: 'Copying Items', getMessage: process => `${process.completed ? 'Copied' : 'Copying'} ${process.data.count} ${pluralize('item', process.data.count)}`, }, + 'metadata-retrieval': { + title: 'Retrieving Metadata', + skipSpinner: true, + getMessage: process => `${process.completed ? 'Retrieved' : 'Retrieving'} metadata for ${process.data.count} ${pluralize('item', process.data.count)}`, + action: (process, dispatch) => { + dispatch(toggleModal(METADATA_RETRIEVAL, true)); + }, + getActionLabel: () => 'View', + }, }; const OngoingProcessDescription = ({ process }) => { const dispatch = useDispatch(); const handleActionClick = useCallback(() => { - dispatch({ type: 'CLEAR_ONGOING', id: process.id }); - PROCESSES[process.kind]?.action(process, dispatch); + (PROCESSES[process.kind]?.action ?? defaultAction)(process, dispatch); }, [dispatch, process]); return (