Skip to content

Commit

Permalink
Add support for metadata retrieval (+ undo) on touch devices
Browse files Browse the repository at this point in the history
  • Loading branch information
tnajdek committed Jan 14, 2025
1 parent 57e679b commit feec651
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 98 deletions.
112 changes: 65 additions & 47 deletions src/js/component/item/actions/more-actions.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ import { Dropdown, DropdownToggle, DropdownMenu, DropdownItem, Icon } from 'web-
import { useDispatch, useSelector } from 'react-redux';

import { cleanDOI, cleanURL, get, getDOIURL } from '../../../utils';
import { currentGoToSubscribeUrl, currentUndoRetrieveMetadata, pickBestItemAction } from '../../../actions';
import { currentGoToSubscribeUrl, pickBestItemAction } from '../../../actions';
import { useItemActionHandlers } from '../../../hooks';
import { READER_CONTENT_TYPES, READER_CONTENT_TYPES_HUMAN_READABLE } from '../../../constants/reader';

const MoreActionsItems = ({ divider = false }) => {
const dispatch = useDispatch();
const isTouchOrSmall = useSelector(state => state.device.isTouchOrSmall);
const isReadOnly = useSelector(state => (state.config.libraries.find(l => l.key === state.current.libraryKey) || {}).isReadOnly);
const item = useSelector(state => get(state, ['libraries', state.current.libraryKey, 'items', state.current.itemKey]));
const itemsSource = useSelector(state => state.current.itemsSource);
const selectedItemsCanBeRecognized = useSelector(state => state.current.itemKeys.every(
key => state.libraries[state.current.libraryKey]?.dataObjects?.[key]?.itemType === 'attachment'
&& state.libraries[state.current.libraryKey]?.dataObjects?.[key]?.contentType === 'application/pdf'
));
const selectedItemsCanBeUnrecognized = useSelector(state =>
state.current.itemKeys.every(
key => !!state.recognize.lookup[`${state.current.libraryKey}-${key}`]
));
const { handleDuplicate } = useItemActionHandlers();
));
const { handleDuplicate, handleRetrieveMetadata, handleUnrecognize } = useItemActionHandlers();

const attachment = get(item, [Symbol.for('links'), 'attachment'], null);
const isViewFile = attachment !== null;
Expand All @@ -31,45 +36,49 @@ const MoreActionsItems = ({ divider = false }) => {
}, [dispatch, item]);

const handleViewOnlineClick = useCallback(() => {
if(url) {
if (url) {
window.open(url);
} else if(doi) {
} else if (doi) {
window.open(getDOIURL(doi));
}
}, [doi, url]);

const handleUnrecognize = useCallback(() => {
dispatch(currentUndoRetrieveMetadata());
}, [dispatch]);

return (
<Fragment>
{ isViewFile && (
<DropdownItem onClick={ handleViewFileClick }>
View {Object.keys(READER_CONTENT_TYPES).includes(attachment.attachmentType) ? READER_CONTENT_TYPES_HUMAN_READABLE[attachment.attachmentType] : 'File' }
<Fragment>
{isViewFile && (
<DropdownItem onClick={handleViewFileClick}>
View {Object.keys(READER_CONTENT_TYPES).includes(attachment.attachmentType) ? READER_CONTENT_TYPES_HUMAN_READABLE[attachment.attachmentType] : 'File'}
</DropdownItem>
) }
{ isViewOnline && (
<DropdownItem onClick={ handleViewOnlineClick }>
View Online
</DropdownItem>
) }
{ canDuplicate && (
<DropdownItem onClick={ handleDuplicate }>
Duplicate Item
</DropdownItem>
) }
{ selectedItemsCanBeUnrecognized && (
)}
{isViewOnline && (
<DropdownItem onClick={handleViewOnlineClick}>
View Online
</DropdownItem>
)}
{canDuplicate && (
<DropdownItem onClick={handleDuplicate}>
Duplicate Item
</DropdownItem>
)}
{(isTouchOrSmall && selectedItemsCanBeRecognized) && (
<>
{(canDuplicate || isViewFile || isViewOnline) && <DropdownItem divider />}
<DropdownItem onClick={handleRetrieveMetadata}>
Retrieve Metadata
</DropdownItem>
</>
)}
{selectedItemsCanBeUnrecognized && (
<>
{(canDuplicate || isViewFile || isViewOnline) && <DropdownItem divider /> }
<DropdownItem onClick={ handleUnrecognize }>
{(canDuplicate || isViewFile || isViewOnline) && <DropdownItem divider />}
<DropdownItem onClick={handleUnrecognize}>
Undo Retrieve Metadata
</DropdownItem>
</>
) }
{divider && (canDuplicate || isViewFile || isViewOnline || selectedItemsCanBeUnrecognized) && <DropdownItem divider/> }
)}
{divider && (canDuplicate || isViewFile || isViewOnline || selectedItemsCanBeUnrecognized) && <DropdownItem divider />}
</Fragment>
);
);
}

const MoreActionsDropdownDesktop = memo(props => {
Expand All @@ -82,13 +91,13 @@ const MoreActionsDropdownDesktop = memo(props => {
}, [isOpen]);

const handleKeyDown = useCallback(ev => {
if(ev.target !== ev.currentTarget) {
if (ev.target !== ev.currentTarget) {
return;
}

if(ev.key === 'ArrowRight') {
if (ev.key === 'ArrowRight') {
onFocusNext(ev);
} else if(ev.key === 'ArrowLeft') {
} else if (ev.key === 'ArrowLeft') {
onFocusPrev(ev);
}
}, [onFocusNext, onFocusPrev]);
Expand All @@ -100,16 +109,16 @@ const MoreActionsDropdownDesktop = memo(props => {
return (
<Dropdown
className="new-item-selector"
isOpen={ isOpen }
onToggle={ handleToggleDropdown }
isOpen={isOpen}
onToggle={handleToggleDropdown}
>
<DropdownToggle
className="btn-icon dropdown-toggle"
onKeyDown={ handleKeyDown }
tabIndex={ tabIndex }
onKeyDown={handleKeyDown}
tabIndex={tabIndex}
title="More"
>
<Icon type={ '16/options' } width="16" height="16" />
<Icon type={'16/options'} width="16" height="16" />
</DropdownToggle>
{
// For performance reasons MoreActionsMenu is only mounted when "more actions" is
Expand All @@ -118,7 +127,7 @@ const MoreActionsDropdownDesktop = memo(props => {
isOpen && (
<DropdownMenu>
<MoreActionsItems divider />
<DropdownItem onClick={ handleSubscribeClick }>
<DropdownItem onClick={handleSubscribeClick}>
Subscribe To Feed
</DropdownItem>
</DropdownMenu>
Expand Down Expand Up @@ -151,8 +160,17 @@ const MoreActionsDropdownTouch = memo(() => {
const doi = item && item.DOI ? cleanDOI(item.DOI) : null;
const isViewOnline = !isViewFile && (url || doi);
const canDuplicate = !isReadOnly && item && (itemsSource === 'collection' || itemsSource === 'top');
const hasAnyAction = isViewFile|| isViewOnline || canDuplicate;

const selectedItemsCanBeRecognized = useSelector(state => state.current.itemKeys.length > 0 && state.current.itemKeys.every(
key => state.libraries[state.current.libraryKey]?.dataObjects?.[key]?.itemType === 'attachment'
&& state.libraries[state.current.libraryKey]?.dataObjects?.[key]?.contentType === 'application/pdf'
));
const selectedItemsCanBeUnrecognized = useSelector(state =>
state.current.itemKeys.length > 0 && state.current.itemKeys.every(
key => !!state.recognize.lookup[`${state.current.libraryKey}-${key}`]
));
const canRecognize = !isReadOnly && selectedItemsCanBeRecognized;
const canUnregonize = !isReadOnly && selectedItemsCanBeUnrecognized;
const hasAnyAction = isViewFile || isViewOnline || canDuplicate || canRecognize || canUnregonize;
const [isOpen, setIsOpen] = useState(false);

const handleDropdownToggle = useCallback(() => {
Expand All @@ -161,23 +179,23 @@ const MoreActionsDropdownTouch = memo(() => {

return (
<Dropdown
isOpen={ isOpen }
onToggle={ handleDropdownToggle }
isOpen={isOpen}
onToggle={handleDropdownToggle}
>
<DropdownToggle
disabled={ !hasAnyAction }
disabled={!hasAnyAction}
className="btn-link btn-icon dropdown-toggle item-actions-touch"
>
<Icon
type="24/options"
symbol={ isOpen ? 'options-block' : 'options' }
symbol={isOpen ? 'options-block' : 'options'}
width="24"
height="24"
/>
</DropdownToggle>
{ hasAnyAction && ( <DropdownMenu>
{hasAnyAction && (<DropdownMenu>
<MoreActionsItems />
</DropdownMenu> ) }
</DropdownMenu>)}
</Dropdown>
)
});
Expand Down
36 changes: 19 additions & 17 deletions src/js/component/modal/metadata-retrieval.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { usePrevious } from 'web-common/hooks';

import Modal from '../ui/modal';
import { METADATA_RETRIEVAL } from '../../constants/modals';
import { currentRetrieveMetadata, toggleModal } from '../../actions';
import { currentRetrieveMetadata, toggleModal, navigate } from '../../actions';


const MetadataRetrievalModal = () => {
Expand All @@ -22,18 +22,20 @@ const MetadataRetrievalModal = () => {
}, [dispatch]);

useEffect(() => {
if(isOpen && !wasOpen) {
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);
}
}, [dispatch, isOpen, wasOpen]);

return (
<Modal
className={ "recognize-modal" }
<Modal
className={"recognize-modal"}
contentLabel="Metadata Retrieval"
isOpen={ isOpen }
onRequestClose={ handleCancel }
overlayClassName={ cx({ 'modal-centered modal-slide': isTouchOrSmall }) }
isOpen={isOpen}
onRequestClose={handleCancel}
overlayClassName={cx({ 'modal-centered modal-slide': isTouchOrSmall })}
>
<div className="modal-header">
{
Expand All @@ -47,7 +49,7 @@ const MetadataRetrievalModal = () => {
<div className="modal-header-right">
<Button
className="btn-link"
onClick={ handleCancel }
onClick={handleCancel}
>
Close
</Button>
Expand All @@ -61,20 +63,20 @@ const MetadataRetrievalModal = () => {
<Button
icon
className="close"
onClick={ handleCancel }
onClick={handleCancel}
>
<Icon type={ '16/close' } width="16" height="16" />
<Icon type={'16/close'} width="16" height="16" />
</Button>
</Fragment>
)
}
</div>
<div
className="modal-body"
tabIndex={ !isTouchOrSmall ? 0 : null }
tabIndex={!isTouchOrSmall ? 0 : null}
>
<div className="recognize-progress">
<progress value={ recognizeProgress } max="1" />
<progress value={recognizeProgress} max="1" />
</div>
<div className="recognize-table">
{recognizeEntries.map(recognize => {
Expand All @@ -83,22 +85,22 @@ const MetadataRetrievalModal = () => {

return (
<div
key={ key }
className={ cx('recognize-row') }
key={key}
className={cx('recognize-row')}
>
<div className="recognize-row-left">
{itemTitle }
{itemTitle}
</div>
<div className="recognize-row-right">
{completed ? parentItemTitle : error ? `Error: ${error}` : "Processing" }
{completed ? parentItemTitle : error ? `Error: ${error}` : "Processing"}
</div>
</div>
);
})}
</div>
</div>
</Modal>
);
);
}

export default memo(MetadataRetrievalModal);
57 changes: 28 additions & 29 deletions src/js/component/touch-footer.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,65 +12,64 @@ const TouchFooter = () => {
const selectedItemsCount = useSelector(state => state.current.itemKeys.length);
const isTrash = useSelector(state => state.current.isTrash);
const collectionKey = useSelector(state => state.current.collectionKey);

const { handleAddToCollectionModalOpen, handleCiteModalOpen, handleRemoveFromCollection,
handleTrash, handlePermanentlyDelete, handleUndelete, handleBibliographyModalOpen,
handleExportModalOpen } = useItemActionHandlers();
handleTrash, handlePermanentlyDelete, handleUndelete, handleBibliographyModalOpen,
handleExportModalOpen } = useItemActionHandlers();

return (
<footer className="touch-footer">
<footer className="touch-footer">
<Toolbar>
<div className="toolbar-justified">
<ToolGroup>
{ !isReadOnly && (
{!isReadOnly && (
<Fragment>
{
!isTrash && (
<Button icon onClick={ handleAddToCollectionModalOpen } disabled={ selectedItemsCount === 0 }>
<Icon type={ '32/add-to-collection' } width="32" height="32" />
</Button>
)}
<Button icon onClick={handleAddToCollectionModalOpen} disabled={selectedItemsCount === 0}>
<Icon type={'32/add-to-collection'} width="32" height="32" />
</Button>
)}
{
(itemsSource === 'collection' || (itemsSource === 'query' && collectionKey)) && (
<Button icon onClick={ handleRemoveFromCollection } disabled={ selectedItemsCount === 0 }>
<Icon type={ '32/remove-from-collection' } width="32" height="32" />
<Button icon onClick={handleRemoveFromCollection} disabled={selectedItemsCount === 0}>
<Icon type={'32/remove-from-collection'} width="32" height="32" />
</Button>
)}
)}
{
!isTrash && (
<Button icon onClick={ handleTrash } disabled={ selectedItemsCount === 0 } >
<Icon type={ '24/trash' } width="24" height="24" />
<Button icon onClick={handleTrash} disabled={selectedItemsCount === 0} >
<Icon type={'24/trash'} width="24" height="24" />
</Button>
)}
)}
{
isTrash && (
<Button icon onClick={ handleUndelete } disabled={ selectedItemsCount === 0 } >
<Icon type={ '24/restore' } width="24" height="24" />
<Button icon onClick={handleUndelete} disabled={selectedItemsCount === 0} >
<Icon type={'24/restore'} width="24" height="24" />
</Button>
)}
)}
{
isTrash && (
<Button icon onClick={ handlePermanentlyDelete } disabled={ selectedItemsCount === 0 } >
<Icon type={ '24/empty-trash' } width="24" height="24" />
<Button icon onClick={handlePermanentlyDelete} disabled={selectedItemsCount === 0} >
<Icon type={'24/empty-trash'} width="24" height="24" />
</Button>
)}
)}
</Fragment>
) }
<Button icon onClick={ handleExportModalOpen } disabled={ selectedItemsCount === 0 || selectedItemsCount > 100 }>
<Icon type={ '24/export' } width="24" height="24" />
)}
<Button icon onClick={handleExportModalOpen} disabled={selectedItemsCount === 0 || selectedItemsCount > 100}>
<Icon type={'24/export'} width="24" height="24" />
</Button>
<Button icon onClick={ handleCiteModalOpen } disabled={ selectedItemsCount === 0 || selectedItemsCount > 100 }>
<Icon type={ '24/cite' } width="24" height="24" />
<Button icon onClick={handleCiteModalOpen} disabled={selectedItemsCount === 0 || selectedItemsCount > 100}>
<Icon type={'24/cite'} width="24" height="24" />
</Button>
<Button icon onClick={ handleBibliographyModalOpen } disabled={ selectedItemsCount === 0 || selectedItemsCount > 100 }>
<Icon type={ '24/bibliography' } width="24" height="24" />
<Button icon onClick={handleBibliographyModalOpen} disabled={selectedItemsCount === 0 || selectedItemsCount > 100}>
<Icon type={'24/bibliography'} width="24" height="24" />
</Button>
<MoreActionsDropdownTouch />
</ToolGroup>
</div>
</Toolbar>
</footer>
);
);
}

export default memo(TouchFooter);
Loading

0 comments on commit feec651

Please sign in to comment.