Skip to content

Commit

Permalink
Enable minimising metadata retrieval as a background task
Browse files Browse the repository at this point in the history
  • Loading branch information
tnajdek committed Jan 22, 2025
1 parent 35f332c commit 04521e1
Show file tree
Hide file tree
Showing 8 changed files with 111 additions and 48 deletions.
51 changes: 42 additions & 9 deletions src/js/actions/current.js
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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', {}
Expand All @@ -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;
Expand All @@ -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;
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/js/actions/recognize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
25 changes: 20 additions & 5 deletions src/js/component/modal/metadata-retrieval.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand Down
55 changes: 35 additions & 20 deletions src/js/component/ongoing.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 (
<li className="process">
<div className="ongoing-text">
{ PROCESSES[process.kind].getMessage(process) }
{PROCESSES[process.kind].getMessage(process)}
</div>
<span className="process-status">
{ process.completed ? (
{(PROCESSES[process.kind].skipSpinner || process.completed) ? (
<Button
onClick={ handleActionClick }
onClick={handleActionClick}
className="btn-link"
>
{ PROCESSES[process.kind]?.getActionLabel?.(process) ?? 'Dismiss' }
{PROCESSES[process.kind]?.getActionLabel?.(process) ?? 'Dismiss'}
</Button>
) :
<Spinner className="small" /> }
<Spinner className="small" />}
</span>
</li>
);
Expand All @@ -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);
Expand Down Expand Up @@ -128,7 +143,7 @@ const Ongoing = () => {

return (
<>
{ modalOpen && (
{modalOpen && (
<Modal
className="modal-touch ongoing-modal"
contentLabel="Upload files"
Expand All @@ -138,16 +153,16 @@ const Ongoing = () => {
overlayClassName="modal-centered modal-slide"
>
</Modal>
) }
<ul
style={ processes.length > 0 ? { height: `${6 + 37 * processes.length}px` } : {} }
className={ cx("ongoing-pane hidden-touch hidden-sm-down", { flash }) }
>
{ (processes || []).map(process => (
<OngoingProcessDescription key={process.id} process={process} />
)) }
</ul>
</>
)}
<ul
style={processes.length > 0 ? { height: `${6 + 37 * processes.length}px` } : {}}
className={cx("ongoing-pane hidden-touch hidden-sm-down", { flash })}
>
{(processes || []).map(process => (
<OngoingProcessDescription key={process.id} process={process} />
))}
</ul>
</>
);
}

Expand Down
1 change: 1 addition & 0 deletions src/js/constants/actions.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
2 changes: 1 addition & 1 deletion src/js/hooks/use-item-action-handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down
6 changes: 4 additions & 2 deletions src/js/reducers/ongoing.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
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';


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) {
Expand Down
15 changes: 6 additions & 9 deletions src/js/reducers/recognize.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)),
{
Expand Down Expand Up @@ -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;
}
Expand Down

0 comments on commit 04521e1

Please sign in to comment.