mirror of
https://github.com/zotero/zotero.git
synced 2026-10-03 02:21:49 +00:00
Show recently used collections in the Add to Collection menu (#6036)
Collections are now tracked as they're selected, added to, or dropped on, and the five most recent usable targets are listed above the full collection hierarchy by full path.
This commit is contained in:
parent
4a8a88dd41
commit
a0ce7b3256
5 changed files with 228 additions and 1 deletions
|
|
@ -2314,6 +2314,10 @@ var CollectionTree = class CollectionTree extends LibraryTree {
|
|||
var targetLibraryID = targetTreeRow.ref.libraryID;
|
||||
var targetCollectionID = targetTreeRow.isCollection() ? targetTreeRow.ref.id : false;
|
||||
|
||||
if (targetCollectionID) {
|
||||
Zotero.Collections.addToRecent(targetTreeRow.ref);
|
||||
}
|
||||
|
||||
if (dataType == 'zotero/collection') {
|
||||
let droppedCollections = await Zotero.Collections.getAsync(data);
|
||||
if (droppedCollections.some(c => c.id == targetCollectionID)) {
|
||||
|
|
|
|||
|
|
@ -160,6 +160,67 @@ Zotero.Collections = function () {
|
|||
}
|
||||
|
||||
|
||||
/**
|
||||
* The number of collections tracked in the recently used list for each library
|
||||
*/
|
||||
this.MAX_RECENT = 20;
|
||||
|
||||
/**
|
||||
* The number of collections tracked in the recently used list across all libraries
|
||||
*/
|
||||
this.MAX_RECENT_TOTAL = 100;
|
||||
|
||||
|
||||
// Collection ids, most recent first, for the current session only
|
||||
var _recent = [];
|
||||
|
||||
|
||||
/**
|
||||
* Get the collections the user has most recently viewed or added items to, most recent first
|
||||
*
|
||||
* @param {Integer} [libraryID] - Limit to collections in a given library
|
||||
* @return {Zotero.Collection[]}
|
||||
*/
|
||||
this.getRecent = function (libraryID) {
|
||||
var collections = [];
|
||||
for (let id of _recent) {
|
||||
// A collection can be deleted or trashed after it's added to the list
|
||||
let collection = this.get(id);
|
||||
if (!collection || collection.deleted) {
|
||||
continue;
|
||||
}
|
||||
if (libraryID === undefined || collection.libraryID == libraryID) {
|
||||
collections.push(collection);
|
||||
}
|
||||
}
|
||||
return collections;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Move a collection to the front of the recently used list
|
||||
*
|
||||
* @param {Zotero.Collection} collection
|
||||
*/
|
||||
this.addToRecent = function (collection) {
|
||||
_recent = _recent.filter(id => id != collection.id);
|
||||
_recent.unshift(collection.id);
|
||||
// Drop collections that have been deleted, along with everything from a library that
|
||||
// has been removed, and trim each library separately, so that heavy use of one doesn't
|
||||
// push another's collections off the end of the list
|
||||
var counts = new Map();
|
||||
_recent = _recent.filter((id) => {
|
||||
let collection = this.get(id);
|
||||
if (!collection) {
|
||||
return false;
|
||||
}
|
||||
let count = (counts.get(collection.libraryID) || 0) + 1;
|
||||
counts.set(collection.libraryID, count);
|
||||
return count <= this.MAX_RECENT;
|
||||
}).slice(0, this.MAX_RECENT_TOTAL);
|
||||
};
|
||||
|
||||
|
||||
this._loadChildCollections = async function (libraryID, ids, idSQL) {
|
||||
var sql = "SELECT C1.collectionID, C2.collectionID AS childCollectionID "
|
||||
+ "FROM collections C1 LEFT JOIN collections C2 ON (C1.collectionID=C2.parentCollectionID) "
|
||||
|
|
|
|||
|
|
@ -62,6 +62,8 @@ var ZoteroPane = new function () {
|
|||
|| ev.getModifierState("Control") || ev.getModifierState("OS");
|
||||
|
||||
const TAB_NUMBER_CODE_RE = /^(?:Numpad|Digit)([0-9])$/;
|
||||
|
||||
const RECENT_COLLECTIONS_SHOWN = 5;
|
||||
|
||||
var self = this,
|
||||
_loaded = false, _madeVisible = false,
|
||||
|
|
@ -1893,6 +1895,11 @@ var ZoteroPane = new function () {
|
|||
await this.itemsView.changeCollectionTreeRows(collectionTreeRows);
|
||||
|
||||
Zotero.Prefs.set('lastViewedFolder', collectionTreeRows[0].id);
|
||||
// The focused row is the one just clicked, whereas collectionTreeRows is in tree order
|
||||
let focusedRow = this.collectionsView.selectedTreeRow;
|
||||
if (focusedRow && focusedRow.isCollection()) {
|
||||
Zotero.Collections.addToRecent(focusedRow.ref);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
|
@ -4849,6 +4856,19 @@ var ZoteroPane = new function () {
|
|||
}
|
||||
};
|
||||
|
||||
|
||||
function _getCollectionPathLabel(collection) {
|
||||
var names = [collection.name];
|
||||
var parentID = collection.parentID;
|
||||
while (parentID) {
|
||||
let parent = Zotero.Collections.get(parentID);
|
||||
names.unshift(parent.name);
|
||||
parentID = parent.parentID;
|
||||
}
|
||||
return names.join(' \u203A ');
|
||||
}
|
||||
|
||||
|
||||
this.buildAddItemToCollectionMenu = function (event, items = this.getSelectedItems()) {
|
||||
if (event.target !== event.currentTarget) return;
|
||||
let popup = event.target;
|
||||
|
|
@ -4873,6 +4893,27 @@ var ZoteroPane = new function () {
|
|||
throw new Error('All items must be the same library');
|
||||
}
|
||||
|
||||
let containsItems = collection => items.every(item => collection.hasItem(item));
|
||||
|
||||
// Recently used collections above the full list, skipping any that already contain
|
||||
// all the items so that every slot is a usable target
|
||||
let recent = Zotero.Collections.getRecent(libraryID)
|
||||
.filter(collection => !containsItems(collection))
|
||||
.slice(0, RECENT_COLLECTIONS_SHOWN);
|
||||
if (recent.length) {
|
||||
let menuitems = recent.map((collection) => {
|
||||
let menuitem = document.createXULElement('menuitem');
|
||||
// Full path, since collections in different parts of the tree can share a name
|
||||
menuitem.setAttribute('label', _getCollectionPathLabel(collection));
|
||||
menuitem.setAttribute('image', collection.treeViewImage);
|
||||
menuitem.classList.add('menuitem-iconic');
|
||||
menuitem.addEventListener('command',
|
||||
() => this.addItemsToCollection(items, collection));
|
||||
return menuitem;
|
||||
});
|
||||
popup.append(...menuitems, document.createXULElement('menuseparator'));
|
||||
}
|
||||
|
||||
let collections = Zotero.Collections.getByLibrary(libraryID);
|
||||
for (let col of collections) {
|
||||
let menuItem = Zotero.Utilities.Internal.createMenuForTarget(
|
||||
|
|
@ -4885,7 +4926,7 @@ var ZoteroPane = new function () {
|
|||
event.stopPropagation();
|
||||
}
|
||||
},
|
||||
collection => items.every(item => collection.hasItem(item))
|
||||
containsItems
|
||||
);
|
||||
popup.append(menuItem);
|
||||
}
|
||||
|
|
@ -4918,6 +4959,7 @@ var ZoteroPane = new function () {
|
|||
);
|
||||
await collection.addItems(ids);
|
||||
});
|
||||
Zotero.Collections.addToRecent(collection);
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -101,4 +101,62 @@ describe("Zotero.Collections", function () {
|
|||
assert.notInstanceOf(collection, Zotero.Feed);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getRecent()", function () {
|
||||
it("should list collections most recently added first", async function () {
|
||||
var col1 = await createDataObject('collection');
|
||||
var col2 = await createDataObject('collection');
|
||||
|
||||
Zotero.Collections.addToRecent(col1);
|
||||
Zotero.Collections.addToRecent(col2);
|
||||
assert.sameOrderedMembers(Zotero.Collections.getRecent().slice(0, 2), [col2, col1]);
|
||||
|
||||
// Adding a collection again moves it to the front without duplicating it
|
||||
Zotero.Collections.addToRecent(col1);
|
||||
assert.sameOrderedMembers(Zotero.Collections.getRecent().slice(0, 2), [col1, col2]);
|
||||
});
|
||||
|
||||
it("should drop the oldest collection past the maximum for each library", async function () {
|
||||
var group = await createGroup();
|
||||
var groupCollection = await createDataObject('collection', { libraryID: group.libraryID });
|
||||
Zotero.Collections.addToRecent(groupCollection);
|
||||
|
||||
var collections = [];
|
||||
for (let i = 0; i <= Zotero.Collections.MAX_RECENT; i++) {
|
||||
let collection = await createDataObject('collection');
|
||||
collections.push(collection);
|
||||
Zotero.Collections.addToRecent(collection);
|
||||
}
|
||||
|
||||
var recent = Zotero.Collections.getRecent(Zotero.Libraries.userLibraryID);
|
||||
assert.lengthOf(recent, Zotero.Collections.MAX_RECENT);
|
||||
assert.notInclude(recent, collections[0]);
|
||||
assert.include(recent, collections[1]);
|
||||
// Another library's collections aren't pushed out along with them
|
||||
assert.include(Zotero.Collections.getRecent(group.libraryID), groupCollection);
|
||||
});
|
||||
|
||||
it("should skip trashed collections and collections in other libraries", async function () {
|
||||
var group = await createGroup();
|
||||
var groupCollection = await createDataObject('collection', { libraryID: group.libraryID });
|
||||
var trashed = await createDataObject('collection');
|
||||
var collection = await createDataObject('collection');
|
||||
|
||||
Zotero.Collections.addToRecent(groupCollection);
|
||||
Zotero.Collections.addToRecent(trashed);
|
||||
Zotero.Collections.addToRecent(collection);
|
||||
|
||||
trashed.deleted = true;
|
||||
await trashed.saveTx();
|
||||
|
||||
var recent = Zotero.Collections.getRecent();
|
||||
assert.include(recent, collection);
|
||||
assert.include(recent, groupCollection);
|
||||
assert.notInclude(recent, trashed);
|
||||
|
||||
var userRecent = Zotero.Collections.getRecent(Zotero.Libraries.userLibraryID);
|
||||
assert.include(userRecent, collection);
|
||||
assert.notInclude(userRecent, groupCollection);
|
||||
});
|
||||
});
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1136,6 +1136,68 @@ describe("ZoteroPane", function () {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#buildAddItemToCollectionMenu()", function () {
|
||||
var popup;
|
||||
|
||||
before(function () {
|
||||
popup = doc.getElementById('zotero-add-to-collection-popup');
|
||||
});
|
||||
|
||||
beforeEach(async function () {
|
||||
// Leave the tree on the library root, so that a collection created later in a test
|
||||
// can't be recorded by the reselection that follows adding a row
|
||||
await selectLibrary(win);
|
||||
});
|
||||
|
||||
after(async function () {
|
||||
// Don't leave a multiple selection behind for later tests
|
||||
await selectLibrary(win);
|
||||
});
|
||||
|
||||
it("should record the selected collection as recently used", async function () {
|
||||
var collection = await createDataObject('collection');
|
||||
await zp.collectionsView.selectByID("C" + collection.id);
|
||||
await waitForItemsLoad(win);
|
||||
|
||||
assert.equal(Zotero.Collections.getRecent()[0], collection);
|
||||
});
|
||||
|
||||
it("should record the collection added to a multiple selection", async function () {
|
||||
var c1 = await createDataObject('collection', { name: 'AAA recent' });
|
||||
var c2 = await createDataObject('collection', { name: 'ZZZ recent' });
|
||||
var cv = zp.collectionsView;
|
||||
await cv.selectByID("C" + c1.id);
|
||||
await waitForItemsLoad(win);
|
||||
cv.selection.toggleSelect(cv.getRowIndexByID("C" + c2.id));
|
||||
await zp.onCollectionSelected();
|
||||
await zp.itemsView.waitForLoad();
|
||||
|
||||
assert.equal(Zotero.Collections.getRecent()[0], c2);
|
||||
});
|
||||
|
||||
it("should offer recently used collections above the full list", async function () {
|
||||
var parent = await createDataObject('collection', { name: 'Parent' });
|
||||
var child = await createDataObject('collection', { name: 'Child', parentID: parent.id });
|
||||
var item = await createDataObject('item', { collections: [parent.id] });
|
||||
|
||||
Zotero.Collections.addToRecent(parent);
|
||||
Zotero.Collections.addToRecent(child);
|
||||
|
||||
zp.buildAddItemToCollectionMenu({ target: popup, currentTarget: popup }, [item]);
|
||||
|
||||
// Recent collections follow New Collection and a separator, by full path
|
||||
var nodes = [...popup.children];
|
||||
var recent = [];
|
||||
for (let node of nodes.slice(nodes.findIndex(n => n.tagName == 'menuseparator') + 1)) {
|
||||
if (node.tagName == 'menuseparator') break;
|
||||
recent.push(node.getAttribute('label'));
|
||||
}
|
||||
assert.include(recent, 'Parent \u203A Child');
|
||||
// The parent already contains the item, so it isn't offered
|
||||
assert.notInclude(recent, 'Parent');
|
||||
});
|
||||
});
|
||||
|
||||
describe("#buildItemContextMenu()", function () {
|
||||
it("shouldn't show export or bib options for multiple standalone file attachments without notes", async function () {
|
||||
var item1 = await importFileAttachment('test.png');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue