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 (
  • - { PROCESSES[process.kind].getMessage(process) } + {PROCESSES[process.kind].getMessage(process)}
    - { process.completed ? ( + {(PROCESSES[process.kind].skipSpinner || process.completed) ? ( ) : - } + }
  • ); @@ -68,7 +83,7 @@ OngoingProcessDescription.propTypes = { const Ongoing = () => { const dispatch = useDispatch(); - const processes = useSelector(state => state.ongoing); + const processes = useSelector(state => state.ongoing).filter(p => !p.skipUI); const activeProcesses = processes.filter(p => !p.completed); const prevActiveProcesses = usePrevious(activeProcesses); const isTouchOrSmall = useSelector(state => state.device.isTouchOrSmall); @@ -128,7 +143,7 @@ const Ongoing = () => { return ( <> - { modalOpen && ( + {modalOpen && ( { overlayClassName="modal-centered modal-slide" > - ) } - - + )} + + ); } diff --git a/src/js/constants/actions.js b/src/js/constants/actions.js index 444a0694..1cc93ee9 100644 --- a/src/js/constants/actions.js +++ b/src/js/constants/actions.js @@ -271,4 +271,5 @@ export const TRIGGER_SEARCH_MODE = 'TRIGGER_SEARCH_MODE'; export const TRIGGER_SELECT_MODE = 'TRIGGER_SELECT_MODE'; export const TRIGGER_USER_TYPE_CHANGE = 'TRIGGER_USER_TYPE_CHANGE'; export const TRIGGER_VIEWPORT_CHANGE = 'TRIGGER_VIEWPORT_CHANGE'; +export const UPDATE_ONGOING = 'UPDATE_ONGOING'; export const UPDATE_RECOGNIZE_DOCUMENT = 'UPDATE_RECOGNIZE_DOCUMENT'; diff --git a/src/js/hooks/use-item-action-handlers.js b/src/js/hooks/use-item-action-handlers.js index b99f8341..96f3b13e 100644 --- a/src/js/hooks/use-item-action-handlers.js +++ b/src/js/hooks/use-item-action-handlers.js @@ -81,7 +81,7 @@ const useItemActionHandlers = () => { }, [dispatch]); const handleRetrieveMetadata = useCallback(() => { - dispatch(toggleModal(METADATA_RETRIEVAL, true)); + dispatch(toggleModal(METADATA_RETRIEVAL, true, { recognizeSelected: true })); }, [dispatch]); const handleUnrecognize = useCallback(() => { diff --git a/src/js/reducers/ongoing.js b/src/js/reducers/ongoing.js index 53218d3a..2bbbb4e6 100644 --- a/src/js/reducers/ongoing.js +++ b/src/js/reducers/ongoing.js @@ -1,4 +1,4 @@ -import { BEGIN_ONGOING, CLEAR_ONGOING, COMPLETE_ONGOING } from '../constants/actions'; +import { BEGIN_ONGOING, CLEAR_ONGOING, COMPLETE_ONGOING, UPDATE_ONGOING } from '../constants/actions'; import { pick } from 'web-common/utils'; @@ -6,8 +6,10 @@ const ongoing = (state = [], action) => { if(action.type === BEGIN_ONGOING) { return [ ...state.filter(p => !p.completed), // remove completed processes when adding a new one - { completed: false, ...pick(action, ['id', 'kind', 'data']) } + { completed: false, ...pick(action, ['id', 'kind', 'data', 'skipUI']) } ]; + } else if(action.type === UPDATE_ONGOING) { + return state.map(p => p.id === action.id ? { ...p, ...pick(action, ['kind', 'skipUI']), data: { ...p.data, ...action.data } } : p); } else if(action.type === COMPLETE_ONGOING) { return state.map(p => p.id === action.id ? { ...p, completed: true, data: { ...p.data, ...action.data } } : p); } else if(action.type === CLEAR_ONGOING) { diff --git a/src/js/reducers/recognize.js b/src/js/reducers/recognize.js index c27bbb4d..6355c7fd 100644 --- a/src/js/reducers/recognize.js +++ b/src/js/reducers/recognize.js @@ -17,17 +17,19 @@ const updateProgressInState = (state) => { }; } -const defaultState = { +const getDefaultState = () => ({ + backgroundTaskId: null, // id of the background task for all recognition processes, can only be updated by BEGIN_RECOGNIZE_DOCUMENT action progress: 0, entries: [], // items being recognized: { itemKey, itemTitle, libraryKey, stage, error, completed }, lookup: {}, // items previously recognized: { libraryKey-itemKey: parentItemKey } -} +}); -const recognize = (state = defaultState, action, globalState) => { +const recognize = (state = getDefaultState(), action, globalState) => { switch (action.type) { case BEGIN_RECOGNIZE_DOCUMENT: return updateProgressInState({ ...state, + backgroundTaskId: action.backgroundTaskId, entries: [ ...state.entries.filter(entry => !(entry.itemKey === action.itemKey && entry.libraryKey === action.libraryKey)), { @@ -111,12 +113,7 @@ const recognize = (state = defaultState, action, globalState) => { lookup: omit(state.lookup, `${action.libraryKey}-${action.itemKey}`), }); case CLEAR_RECOGNIZE_DOCUMENTS: - return { - ...state, - progress: 0, - entries: [], - lookup: {}, - } + return getDefaultState(); default: return state; }