Skip to content

Commit

Permalink
Warn when adding a duplicate item. Resolve zotero#269
Browse files Browse the repository at this point in the history
Item is considered a potential duplicate when any of the following
properties is identical on both items: ISBN, DOI, ISSN, url, title.
  • Loading branch information
tnajdek committed Aug 5, 2021
1 parent 0559da1 commit 73da30a
Show file tree
Hide file tree
Showing 4 changed files with 46 additions and 7 deletions.
29 changes: 28 additions & 1 deletion src/js/components/container.jsx
Expand Up @@ -5,7 +5,7 @@ import SmoothScroll from 'smooth-scroll';
import PropTypes from 'prop-types';

import { calcOffset, dedupMultipleChoiceItems, ensureNoBlankItems, fetchFromPermalink,
getOneTimeBibliographyOrFallback, getExpandedCitationStyles, getItemsCSL, isLikeUrl,
getOneTimeBibliographyOrFallback, getExpandedCitationStyles, getItemsCSL, isDuplicate, isLikeUrl,
parseIdentifier, processMultipleChoiceItems, processSentenceCaseAPAItems, retrieveStylesData,
saveToPermalink, validateItem, validateUrl } from '../utils';
import { coreCitationStyles } from '../../../data/citation-styles-data.json';
Expand Down Expand Up @@ -138,6 +138,7 @@ const BibWebContainer = props => {
const copyDataInclude = useRef(null);
const revertCitationStyle = useRef(null);
const lastDeletedItem = useRef(null);
const duplicate = useRef(null);
const [isDataReady, setIsDataReady] = useState(false);
const [activeDialog, setActiveDialog] = useState(null);
const wasDataReady = usePrevious(isDataReady);
Expand Down Expand Up @@ -315,6 +316,17 @@ const BibWebContainer = props => {
}, [state.styleHasBibliography, state.bibliography]);

const addItem = useCallback((item, showFirstCitationMessage = true) => {
duplicate.current = isDuplicate(item, bib.current.itemsRaw);
if(duplicate.current) {
const message = {
action: 'Show Duplicate',
id: getNextMessageId(),
kind: 'DUPLICATE',
message: 'Possible duplicate already exists in the bibliography',
};
dispatch({ type: REPLACE_MESSAGE, kind: 'DUPLICATE', message });
}

if(state.isSentenceCaseStyle) {
bib.current.addItem(processSentenceCaseAPAItems([item])[0]);
} else {
Expand Down Expand Up @@ -765,6 +777,20 @@ const BibWebContainer = props => {
dispatch({ type: CLEAR_MESSAGE, kind: 'WELCOME_MESSAGE' });
}, []);

const handleShowDuplicate = useCallback(event => {
if(duplicate.current) {
setActiveDialog(null);
const target = document.querySelector(`[data-key="${duplicate.current.key}"]`);
(new SmoothScroll()).animateScroll(target, event.currentTarget, {
header: '.message',
offset: calcOffset(),
speed: 1000, speedAsDuration: true,
});
duplicate.current = null;
}
dispatch({ type: CLEAR_MESSAGE, kind: 'DUPLICATE' });
}, []);

const handleStyleInstallerCancel = () => {
setActiveDialog(null);
};
Expand Down Expand Up @@ -1158,6 +1184,7 @@ const BibWebContainer = props => {
onReviewDismiss = { handleReviewDismiss }
onReviewEdit = { handleReviewEdit }
onSave = { handleSave }
onShowDuplicate={ handleShowDuplicate }
onStyleInstallerCancel = { handleStyleInstallerCancel }
onStyleInstallerDelete = { handleStyleInstallerDelete }
onStyleInstallerSelect = { handleStyleInstallerSelect }
Expand Down
8 changes: 5 additions & 3 deletions src/js/components/message.jsx
Expand Up @@ -5,22 +5,24 @@ import cx from 'classnames';
import Icon from './ui/icon';
import Button from './ui/button';

const Message = ({ action, id, message, kind, href, onDismiss, onReadMore, onUndoDelete }) => {
const Message = ({ action, id, message, kind, href, onDismiss, onReadMore, onShowDuplicate, onUndoDelete }) => {
let category;

switch(kind) {
case 'UNDO_DELETE': category = 'warning'; break;
case 'FIRST_CITATION': category = 'success'; break;
case 'ERROR': category = 'error'; break;
case 'DUPLICATE': category = 'warning'; break;
default: category = 'info'; break;
}

const handleAction = useCallback(ev => {
switch(kind) {
case 'WELCOME_MESSAGE': onReadMore(ev); break;
case 'UNDO_DELETE': onUndoDelete(); break;
case 'DUPLICATE': onShowDuplicate(ev); break;
}
}, [kind, onReadMore, onUndoDelete]);
}, [kind, onReadMore, onShowDuplicate, onUndoDelete]);

const handleDismiss = useCallback(() => {
onDismiss(id);
Expand Down Expand Up @@ -62,7 +64,7 @@ Message.propTypes = {
id: PropTypes.number,
action: PropTypes.string,
href: PropTypes.string,
kind: PropTypes.oneOf(['ERROR', 'FIRST_CITATION', 'INFO', 'UNDO_DELETE', 'WELCOME_MESSAGE']).isRequired,
kind: PropTypes.oneOf(['DUPLICATE', 'ERROR', 'FIRST_CITATION', 'INFO', 'UNDO_DELETE', 'WELCOME_MESSAGE']).isRequired,
message: PropTypes.oneOfType([PropTypes.string, PropTypes.element]),
onDismiss: PropTypes.func.isRequired,
onReadMore: PropTypes.func,
Expand Down
4 changes: 1 addition & 3 deletions src/js/components/zbib.jsx
Expand Up @@ -46,10 +46,8 @@ class ZBib extends React.PureComponent {
{ this.props.messages.map(message => (
<Message
{ ...message }
{ ...pick(this.props, ['onDismiss', 'onUndoDelete', 'onReadMore', 'onShowDuplicate'])}
key={ message.id }
onDismiss = { this.props.onDismiss }
onUndoDelete = { this.props.onUndoDelete }
onReadMore = { this.props.onReadMore }
/>
))
}
Expand Down
12 changes: 12 additions & 0 deletions src/js/utils.js
Expand Up @@ -406,6 +406,17 @@ const pickBestLocale = (userLocales, supportedLocales, fallback = 'en-US') => {
return fallback;
}

const isDuplicate = (newItem, items = []) => {
const result = items.find(item =>
(item.ISBN && (item.ISBN === newItem.ISBN)) ||
(item.DOI && (item.DOI === newItem.DOI)) ||
(item.ISSN && (item.ISSN === newItem.ISSN)) ||
(item.url && (item.url === newItem.url)) ||
(item.title && (item.title === newItem.title))
);
return result ?? false;
}

export {
calcOffset,
dedupMultipleChoiceItems,
Expand All @@ -417,6 +428,7 @@ export {
getItemsCSL,
getItemTypes,
getOneTimeBibliographyOrFallback,
isDuplicate,
isLikeUrl,
noop,
parseIdentifier,
Expand Down

0 comments on commit 73da30a

Please sign in to comment.